diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c9f7585f45..da014bccde 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -1,2257 +1,2249 @@ array( - 'core.pkg.css' => 'f79ebe46', + 'core.pkg.css' => '33799ec4', 'core.pkg.js' => 'a590b451', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '9451634c', 'differential.pkg.js' => 'ebef29b1', 'diffusion.pkg.css' => '385e85b3', 'diffusion.pkg.js' => '0115b37c', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '3ec6a6d5', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/dark-console.css' => '6378ef3d', 'rsrc/css/aphront/dialog-view.css' => 'fe58b18d', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => 'aa5ffcb9', 'rsrc/css/aphront/multi-column.css' => 'fd18389d', 'rsrc/css/aphront/notification.css' => '9c279160', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'a24cb589', 'rsrc/css/aphront/table-view.css' => 'e3632cc9', 'rsrc/css/aphront/tokenizer.css' => '04875312', 'rsrc/css/aphront/tooltip.css' => '7672b60f', - 'rsrc/css/aphront/two-column.css' => '16ab3ad2', 'rsrc/css/aphront/typeahead-browse.css' => 'd8581d2c', 'rsrc/css/aphront/typeahead.css' => '0e403212', 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '0877ed6e', 'rsrc/css/application/base/main-menu-view.css' => '2f670a96', 'rsrc/css/application/base/notification-menu.css' => 'f31c0bde', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', 'rsrc/css/application/base/phui-theme.css' => '6b451f24', 'rsrc/css/application/base/standard-page-view.css' => '4d176b67', 'rsrc/css/application/calendar/calendar-icon.css' => 'c69aa59f', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '7fedf08b', 'rsrc/css/application/config/config-template.css' => '8e6c6fcd', 'rsrc/css/application/config/config-welcome.css' => '6abd79be', 'rsrc/css/application/config/setup-issue.css' => 'db7e9c40', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', 'rsrc/css/application/conpherence/durable-column.css' => '86396117', 'rsrc/css/application/conpherence/menu.css' => 'f99fee4c', - 'rsrc/css/application/conpherence/message-pane.css' => '2c16d204', + 'rsrc/css/application/conpherence/message-pane.css' => 'dd4f8a3b', 'rsrc/css/application/conpherence/notification.css' => '6cdcc253', 'rsrc/css/application/conpherence/transaction.css' => '85d0974c', 'rsrc/css/application/conpherence/update.css' => 'faf6be09', 'rsrc/css/application/conpherence/widget-pane.css' => '419fd50c', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', - 'rsrc/css/application/countdown/timer.css' => '4f02bd98', + 'rsrc/css/application/countdown/timer.css' => 'e7544472', 'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a', 'rsrc/css/application/dashboard/dashboard.css' => 'eb458607', 'rsrc/css/application/diff/inline-comment-summary.css' => '51efda3a', 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', 'rsrc/css/application/differential/changeset-view.css' => 'b6b0d1bb', 'rsrc/css/application/differential/core.css' => '7ac3cabc', 'rsrc/css/application/differential/phui-inline-comment.css' => '9fadd6b8', 'rsrc/css/application/differential/revision-comment.css' => '14b8565a', 'rsrc/css/application/differential/revision-history.css' => '0e8eb855', 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33', 'rsrc/css/application/differential/table-of-contents.css' => 'ae4b7a55', 'rsrc/css/application/diffusion/diffusion-icons.css' => '4ba18923', 'rsrc/css/application/diffusion/diffusion-readme.css' => '2106ea08', 'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661', 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad', 'rsrc/css/application/flag/flag.css' => '5337623f', 'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4', - 'rsrc/css/application/herald/herald-test.css' => '778b008e', + 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 'rsrc/css/application/herald/herald.css' => '826075fa', 'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5', 'rsrc/css/application/maniphest/report.css' => 'f6931fdf', 'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b', 'rsrc/css/application/maniphest/task-summary.css' => '11cc5344', 'rsrc/css/application/objectselector/object-selector.css' => '85ee8ce6', 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => '1898e534', 'rsrc/css/application/people/people-profile.css' => '25970776', - 'rsrc/css/application/phame/phame.css' => '3259b53d', + 'rsrc/css/application/phame/phame.css' => 'bb147387', 'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => '95174bdd', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02', 'rsrc/css/application/phortune/phortune.css' => '9149f103', 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', 'rsrc/css/application/phriction/phriction-document-css.css' => 'd1861e06', 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', - 'rsrc/css/application/ponder/comments.css' => '865a67e6', - 'rsrc/css/application/ponder/feed.css' => 'e62615b6', - 'rsrc/css/application/ponder/post.css' => '9d415218', - 'rsrc/css/application/ponder/vote.css' => 'aea452b0', + 'rsrc/css/application/ponder/ponder-view.css' => 'fcd6b398', 'rsrc/css/application/projects/project-icon.css' => '4e3eaa5a', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae', 'rsrc/css/application/search/search-results.css' => '7dea472c', 'rsrc/css/application/slowvote/slowvote.css' => '7c27f0f9', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => 'a76cefc9', 'rsrc/css/core/remarkup.css' => 'de13dcbe', 'rsrc/css/core/syntax.css' => '9fd11da8', 'rsrc/css/core/z-index.css' => '57ddcaa2', 'rsrc/css/diviner/diviner-shared.css' => '5a337049', - 'rsrc/css/font/font-awesome.css' => 'e2e712fe', + 'rsrc/css/font/font-awesome.css' => 'd2fc4e8d', 'rsrc/css/font/font-lato.css' => '5ab1a46a', 'rsrc/css/font/font-roboto-slab.css' => 'f24a53cb', 'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 'rsrc/css/layout/phabricator-hovercard-view.css' => '1239cd52', 'rsrc/css/layout/phabricator-side-menu-view.css' => 'bec2458e', 'rsrc/css/layout/phabricator-source-code-view.css' => '5e0178de', 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93', 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338', 'rsrc/css/phui/calendar/phui-calendar-month.css' => '476be7e0', 'rsrc/css/phui/calendar/phui-calendar.css' => 'ccabe893', 'rsrc/css/phui/phui-action-list.css' => 'c5eba19d', 'rsrc/css/phui/phui-action-panel.css' => '3ee9afd5', - 'rsrc/css/phui/phui-badge.css' => 'b6218fa8', + 'rsrc/css/phui/phui-badge.css' => 'f25c3476', 'rsrc/css/phui/phui-box.css' => 'a5bb366d', 'rsrc/css/phui/phui-button.css' => '16020a60', 'rsrc/css/phui/phui-crumbs-view.css' => 'd842f867', 'rsrc/css/phui/phui-document.css' => '0267054b', - 'rsrc/css/phui/phui-feed-story.css' => 'c7d8113a', + 'rsrc/css/phui/phui-feed-story.css' => 'b7b26d23', 'rsrc/css/phui/phui-fontkit.css' => 'cb8ae7ad', 'rsrc/css/phui/phui-form-view.css' => '621b21c5', 'rsrc/css/phui/phui-form.css' => 'afdb2c6e', 'rsrc/css/phui/phui-header-view.css' => '55bb32dd', 'rsrc/css/phui/phui-icon.css' => 'b0a6b1b6', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => '5b16bac6', 'rsrc/css/phui/phui-list.css' => '125599df', 'rsrc/css/phui/phui-object-box.css' => '407eaf5a', - 'rsrc/css/phui/phui-object-item-list-view.css' => 'a1b990b7', + 'rsrc/css/phui/phui-object-item-list-view.css' => '36ce366c', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-property-list-view.css' => 'aeb09581', + 'rsrc/css/phui/phui-property-list-view.css' => '15bbe0b0', 'rsrc/css/phui/phui-remarkup-preview.css' => '867f85b3', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '402691cc', 'rsrc/css/phui/phui-text.css' => 'cf019f54', - 'rsrc/css/phui/phui-timeline-view.css' => 'fc23e7b7', - 'rsrc/css/phui/phui-workboard-view.css' => '6a20991a', - 'rsrc/css/phui/phui-workpanel-view.css' => '8cebb2b1', + 'rsrc/css/phui/phui-timeline-view.css' => 'f1bccf73', + 'rsrc/css/phui/phui-workboard-view.css' => '6704d68d', + 'rsrc/css/phui/phui-workpanel-view.css' => 'adec7699', 'rsrc/css/sprite-login.css' => '1ebb9bf9', 'rsrc/css/sprite-main-header.css' => 'f07bbb87', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-projects.css' => 'e5ad842a', 'rsrc/css/sprite-tokens.css' => '4f399012', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '5fb6fb0e', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => 'a653cb11', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '80526fc8', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '4924d54d', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '7d5a4653', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '531835e8', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '427fe363', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => 'a9897054', 'rsrc/externals/font/lato/lato-bold.eot' => '99fbcf8c', 'rsrc/externals/font/lato/lato-bold.ttf' => '0a7141f7', 'rsrc/externals/font/lato/lato-bold.woff' => 'f5db2061', 'rsrc/externals/font/lato/lato-bold.woff2' => '37a94ecd', 'rsrc/externals/font/lato/lato-bolditalic.eot' => 'b93389d0', 'rsrc/externals/font/lato/lato-bolditalic.ttf' => 'dad31252', 'rsrc/externals/font/lato/lato-bolditalic.woff' => 'e53bcf47', 'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'd035007f', 'rsrc/externals/font/lato/lato-italic.eot' => '6a903f5d', 'rsrc/externals/font/lato/lato-italic.ttf' => '629f64f0', 'rsrc/externals/font/lato/lato-italic.woff' => '678dc4bb', 'rsrc/externals/font/lato/lato-italic.woff2' => '7c8dd650', 'rsrc/externals/font/lato/lato-regular.eot' => '848dfb1e', 'rsrc/externals/font/lato/lato-regular.ttf' => 'e270165b', 'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2', 'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742', 'rsrc/externals/font/robotoslab/robotoslab-regular.eot' => 'eaefe21c', 'rsrc/externals/font/robotoslab/robotoslab-regular.ttf' => '4bfef7d5', 'rsrc/externals/font/robotoslab/robotoslab-regular.woff' => '7f0552f9', 'rsrc/externals/font/robotoslab/robotoslab-regular.woff2' => '23bdd43c', 'rsrc/externals/javelin/core/Event.js' => '85ea0626', 'rsrc/externals/javelin/core/Stratcom.js' => '6c53634d', 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4', 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', 'rsrc/externals/javelin/core/init.js' => '3010e992', 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', 'rsrc/externals/javelin/core/install.js' => '05270951', 'rsrc/externals/javelin/core/util.js' => '93cc50d6', 'rsrc/externals/javelin/docs/Base.js' => '74676256', 'rsrc/externals/javelin/docs/onload.js' => 'e819c479', 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a', 'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba', 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212', 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '2b8de964', 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '1ad0a787', 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed', 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'c90a04fc', 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'fe287620', 'rsrc/externals/javelin/ext/view/View.js' => '0f764c35', 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'f829edb3', 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '47830651', 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2', 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472', 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb', 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b', 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9', 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03', 'rsrc/externals/javelin/lib/DOM.js' => '147805fa', 'rsrc/externals/javelin/lib/History.js' => 'd4505101', 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 'rsrc/externals/javelin/lib/Leader.js' => '331b1611', 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b', 'rsrc/externals/javelin/lib/Quicksand.js' => '4cebc641', 'rsrc/externals/javelin/lib/Request.js' => '94b750d2', 'rsrc/externals/javelin/lib/Resource.js' => '44959b73', 'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692', 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', 'rsrc/externals/javelin/lib/Scrollbar.js' => '087e919c', 'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5', 'rsrc/externals/javelin/lib/URI.js' => '6eff08aa', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', 'rsrc/externals/javelin/lib/WebSocket.js' => 'e292eaf4', 'rsrc/externals/javelin/lib/Workflow.js' => '5b2e3e2b', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68', 'rsrc/externals/javelin/lib/__tests__/URI.js' => '1e45fda9', 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783', 'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a', 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'ab5f468d', 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f', 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', 'rsrc/externals/raphael/raphael.js' => '51ee6b43', 'rsrc/favicons/apple-touch-icon-120x120.png' => '43742962', 'rsrc/favicons/apple-touch-icon-152x152.png' => '669eaec3', 'rsrc/favicons/apple-touch-icon-76x76.png' => 'ecdef672', 'rsrc/favicons/favicon-128.png' => '47cdff03', 'rsrc/favicons/favicon-16x16.png' => 'ee2523ac', 'rsrc/favicons/favicon-32x32.png' => 'b6a8150e', 'rsrc/favicons/favicon-96x96.png' => '8f7ea177', 'rsrc/image/BFCFDA.png' => 'd5ec91f4', 'rsrc/image/actions/edit.png' => '2fc41442', 'rsrc/image/avatar.png' => '3eb28cd9', 'rsrc/image/checker_dark.png' => 'd8e65881', 'rsrc/image/checker_light.png' => 'a0155918', 'rsrc/image/checker_lighter.png' => 'd5da91b6', 'rsrc/image/darkload.gif' => '1ffd3ec6', 'rsrc/image/divot.png' => '94dded62', 'rsrc/image/examples/hero.png' => '979a86ae', 'rsrc/image/grippy_texture.png' => 'aca81e2f', 'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c', 'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0', 'rsrc/image/icon/fatcow/bullet_black.png' => 'ff190031', 'rsrc/image/icon/fatcow/bullet_orange.png' => 'e273e5bb', 'rsrc/image/icon/fatcow/bullet_red.png' => 'c0b75434', 'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275', 'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60', 'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d', 'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee', 'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783', 'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a', 'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66', 'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2', 'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522', 'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f', 'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4', 'rsrc/image/icon/fatcow/folder.png' => '95a435af', 'rsrc/image/icon/fatcow/folder_go.png' => '001cbc94', 'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a', 'rsrc/image/icon/fatcow/link.png' => '7afd4d5e', 'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8', 'rsrc/image/icon/fatcow/page_white_link.png' => 'a90023c7', 'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c', 'rsrc/image/icon/fatcow/page_white_text.png' => '1e1f79c3', 'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f', 'rsrc/image/icon/fatcow/source/email.png' => '9bab3239', 'rsrc/image/icon/fatcow/source/fax.png' => '04195e68', 'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264', 'rsrc/image/icon/fatcow/source/tablet.png' => '49396799', 'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d', 'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8', 'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e', 'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b', 'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3', 'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0', 'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21', 'rsrc/image/icon/subscribe.png' => 'd03ed5a5', 'rsrc/image/icon/tango/attachment.png' => 'ecc8022e', 'rsrc/image/icon/tango/edit.png' => '929a1363', 'rsrc/image/icon/tango/go-down.png' => '96d95e43', 'rsrc/image/icon/tango/log.png' => 'b08cc63a', 'rsrc/image/icon/tango/upload.png' => '7bbb7984', 'rsrc/image/icon/unsubscribe.png' => '25725013', 'rsrc/image/lightblue-header.png' => '5c168b6d', 'rsrc/image/main_texture.png' => '29a2c5ad', 'rsrc/image/menu_texture.png' => '5a17580d', 'rsrc/image/people/harding.png' => '45aa614e', 'rsrc/image/people/jefferson.png' => 'afca0e53', 'rsrc/image/people/lincoln.png' => '9369126d', 'rsrc/image/people/mckinley.png' => 'fb8f16ce', 'rsrc/image/people/taft.png' => 'd7bc402c', 'rsrc/image/people/washington.png' => '40dd301c', 'rsrc/image/phrequent_active.png' => 'a466a8ed', 'rsrc/image/phrequent_inactive.png' => 'bfc15a69', 'rsrc/image/sprite-login-X2.png' => 'a4bf0a98', 'rsrc/image/sprite-login.png' => '5f4d0069', 'rsrc/image/sprite-main-header.png' => '3673af44', 'rsrc/image/sprite-menu-X2.png' => 'cfd8fca5', 'rsrc/image/sprite-menu.png' => 'd7a99faa', 'rsrc/image/sprite-projects-X2.png' => '853552c7', 'rsrc/image/sprite-projects.png' => 'b9dd74b8', 'rsrc/image/sprite-tokens-X2.png' => '348f1745', 'rsrc/image/sprite-tokens.png' => 'ce0b62be', 'rsrc/image/texture/card-gradient.png' => '815f26e8', 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8', 'rsrc/image/texture/dark-menu.png' => '7e22296e', 'rsrc/image/texture/grip.png' => '719404f3', 'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe', 'rsrc/image/texture/phlnx-bg.png' => '8d819209', 'rsrc/image/texture/pholio-background.gif' => 'ba29239c', 'rsrc/image/texture/table_header.png' => '5c433037', 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '031cee25', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'fb20ac8d', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'edd1ba66', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 'rsrc/js/application/calendar/behavior-day-view.js' => '5c46cff2', 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c72aa091', 'rsrc/js/application/conpherence/behavior-menu.js' => 'd3782c93', 'rsrc/js/application/conpherence/behavior-pontificate.js' => '21ba5861', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', 'rsrc/js/application/conpherence/behavior-widget-pane.js' => 'a8458711', 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3', 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145', 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e', 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 'rsrc/js/application/differential/ChangesetViewManager.js' => '58562350', 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'd4c87bf4', 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18', 'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d', 'rsrc/js/application/differential/behavior-comment-preview.js' => 'b064af76', 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '2035b9cb', 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '037b59eb', 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 'rsrc/js/application/differential/behavior-populate.js' => '8694b1df', 'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb', 'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d', 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => 'b42eddc7', 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '9007c197', 'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef', 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => '2b228192', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781', 'rsrc/js/application/files/behavior-icon-composer.js' => '8ef9ab58', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', - 'rsrc/js/application/herald/HeraldRuleEditor.js' => '52684226', + 'rsrc/js/application/herald/HeraldRuleEditor.js' => '91a6031b', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-editor.js' => '782ab6e7', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '7b98d7c5', 'rsrc/js/application/maniphest/behavior-line-chart.js' => '88f0c5b3', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763', 'rsrc/js/application/maniphest/behavior-transaction-controls.js' => '44168bad', 'rsrc/js/application/maniphest/behavior-transaction-expand.js' => '5fefb143', 'rsrc/js/application/maniphest/behavior-transaction-preview.js' => '4c95d29e', 'rsrc/js/application/owners/OwnersPathEditor.js' => 'aa1733d0', 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3', 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc', 'rsrc/js/application/phame/phame-post-preview.js' => 'be807912', 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '246dc085', 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'fbe497e7', 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf', 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c', 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => '7d470398', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b', 'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', 'rsrc/js/application/repository/repository-crossreference.js' => 'bea81850', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f', 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => 'dbbf48b6', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '13c739ea', 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec', 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3', 'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807', 'rsrc/js/application/uiexample/ReactorButtonExample.js' => 'd19198c8', 'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '519705ea', 'rsrc/js/application/uiexample/ReactorFocusExample.js' => '40a6a403', 'rsrc/js/application/uiexample/ReactorInputExample.js' => '886fd850', 'rsrc/js/application/uiexample/ReactorMouseoverExample.js' => '47c794d8', 'rsrc/js/application/uiexample/ReactorRadioExample.js' => '988040b4', 'rsrc/js/application/uiexample/ReactorSelectExample.js' => 'a155550f', 'rsrc/js/application/uiexample/ReactorSendClassExample.js' => '1def2711', 'rsrc/js/application/uiexample/ReactorSendPropertiesExample.js' => 'b1f0ccee', 'rsrc/js/application/uiexample/busy-example.js' => '60479091', 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '07de8873', 'rsrc/js/core/DraggableList.js' => 'a16ec1c6', 'rsrc/js/core/FileUpload.js' => '477359c8', 'rsrc/js/core/Hovercard.js' => '14ac66f5', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', 'rsrc/js/core/Notification.js' => 'ccf1cbf8', 'rsrc/js/core/Prefab.js' => '6920d200', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', 'rsrc/js/core/Title.js' => 'df5e11d2', 'rsrc/js/core/ToolTip.js' => '1d298e3a', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-choose-control.js' => '6153c708', 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', 'rsrc/js/core/behavior-device.js' => 'a205cf28', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e', 'rsrc/js/core/behavior-error-log.js' => '6882e80a', 'rsrc/js/core/behavior-fancy-datepicker.js' => '665cf6ac', 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'c8e57404', 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03', 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', 'rsrc/js/core/behavior-hovercard.js' => 'f36e01af', 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6', 'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7', 'rsrc/js/core/behavior-line-linker.js' => '1499a8cb', 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '49b73b36', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'eeaa9e5a', 'rsrc/js/core/behavior-refresh-csrf.js' => '7814b593', 'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', 'rsrc/js/core/behavior-scrollbar.js' => '834a1173', 'rsrc/js/core/behavior-search-typeahead.js' => '048330fa', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-time-typeahead.js' => 'f80d6bf0', 'rsrc/js/core/behavior-toggle-class.js' => '5d7c9f33', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => '3ee3408b', 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d', 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475', 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', ), 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => '6378ef3d', 'aphront-dialog-view-css' => 'fe58b18d', 'aphront-list-filter-view-css' => 'aa5ffcb9', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', 'aphront-table-view-css' => 'e3632cc9', 'aphront-tokenizer-control-css' => '04875312', 'aphront-tooltip-css' => '7672b60f', - 'aphront-two-column-view-css' => '16ab3ad2', 'aphront-typeahead-control-css' => '0e403212', 'auth-css' => '0877ed6e', 'bulk-job-css' => 'df9c1d4a', 'calendar-icon-css' => 'c69aa59f', 'changeset-view-manager' => '58562350', 'conduit-api-css' => '7bc725c4', 'config-options-css' => '7fedf08b', 'config-welcome-css' => '6abd79be', 'conpherence-durable-column-view' => '86396117', 'conpherence-menu-css' => 'f99fee4c', - 'conpherence-message-pane-css' => '2c16d204', + 'conpherence-message-pane-css' => 'dd4f8a3b', 'conpherence-notification-css' => '6cdcc253', 'conpherence-thread-manager' => '01774ab2', 'conpherence-transaction-css' => '85d0974c', 'conpherence-update-css' => 'faf6be09', 'conpherence-widget-pane-css' => '419fd50c', 'differential-changeset-view-css' => 'b6b0d1bb', 'differential-core-view-css' => '7ac3cabc', 'differential-inline-comment-editor' => 'd4c87bf4', 'differential-revision-add-comment-css' => 'c47f8c40', 'differential-revision-comment-css' => '14b8565a', 'differential-revision-history-css' => '0e8eb855', 'differential-revision-list-css' => 'f3c47d33', 'differential-table-of-contents-css' => 'ae4b7a55', 'diffusion-icons-css' => '4ba18923', 'diffusion-readme-css' => '2106ea08', 'diffusion-source-css' => '66fdf661', 'diviner-shared-css' => '5a337049', - 'font-fontawesome' => 'e2e712fe', + 'font-fontawesome' => 'd2fc4e8d', 'font-lato' => '5ab1a46a', 'font-roboto-slab' => 'f24a53cb', 'global-drag-and-drop-css' => '697324ad', 'harbormaster-css' => '49d64eb4', 'herald-css' => '826075fa', - 'herald-rule-editor' => '52684226', - 'herald-test-css' => '778b008e', + 'herald-rule-editor' => '91a6031b', + 'herald-test-css' => 'a52e323e', 'inline-comment-summary-css' => '51efda3a', 'javelin-aphlict' => '5359e785', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => '031cee25', 'javelin-behavior-aphlict-listen' => 'fb20ac8d', 'javelin-behavior-aphlict-status' => 'ea681761', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-crop' => 'fa0f4fc2', 'javelin-behavior-aphront-drag-and-drop-textarea' => '6d49590e', 'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3', 'javelin-behavior-aphront-more' => 'a80d0378', 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-bulk-job-reload' => 'edf8a145', 'javelin-behavior-choose-control' => '6153c708', 'javelin-behavior-config-reorder-fields' => 'b6993408', 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', 'javelin-behavior-conpherence-menu' => 'd3782c93', 'javelin-behavior-conpherence-pontificate' => '21ba5861', 'javelin-behavior-conpherence-widget-pane' => 'a8458711', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '82439934', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-day-view' => '5c46cff2', 'javelin-behavior-desktop-notifications-control' => 'edd1ba66', 'javelin-behavior-device' => 'a205cf28', 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 'javelin-behavior-differential-comment-jump' => '4fdb476d', 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 'javelin-behavior-differential-dropdown-menus' => '2035b9cb', 'javelin-behavior-differential-edit-inline-comments' => '037b59eb', 'javelin-behavior-differential-feedback-preview' => 'b064af76', 'javelin-behavior-differential-keyboard-navigation' => '2c426492', 'javelin-behavior-differential-populate' => '8694b1df', 'javelin-behavior-differential-toggle-files' => 'ca3f91eb', 'javelin-behavior-differential-user-select' => 'a8d8459d', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '9007c197', 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => '2b228192', 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-durable-column' => 'c72aa091', 'javelin-behavior-error-log' => '6882e80a', 'javelin-behavior-event-all-day' => '38dcf3c8', 'javelin-behavior-fancy-datepicker' => '665cf6ac', 'javelin-behavior-global-drag-and-drop' => 'c8e57404', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', 'javelin-behavior-icon-composer' => '8ef9ab58', 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => 'f8ba29d7', 'javelin-behavior-line-chart' => '88f0c5b3', 'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-maniphest-batch-editor' => '782ab6e7', 'javelin-behavior-maniphest-batch-selector' => '7b98d7c5', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '71237763', 'javelin-behavior-maniphest-transaction-controls' => '44168bad', 'javelin-behavior-maniphest-transaction-expand' => '5fefb143', 'javelin-behavior-maniphest-transaction-preview' => '4c95d29e', 'javelin-behavior-owners-path-editor' => '7a68dda3', 'javelin-behavior-passphrase-credential-control' => '3cb0b2fc', 'javelin-behavior-persona-login' => '9414ff18', 'javelin-behavior-phabricator-active-nav' => 'e379b58e', 'javelin-behavior-phabricator-autofocus' => '7319e029', 'javelin-behavior-phabricator-busy-example' => '60479091', 'javelin-behavior-phabricator-file-tree' => '88236f00', 'javelin-behavior-phabricator-gesture' => '3ab51e2c', 'javelin-behavior-phabricator-gesture-example' => '558829c2', 'javelin-behavior-phabricator-hovercards' => 'f36e01af', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6', 'javelin-behavior-phabricator-line-linker' => '1499a8cb', 'javelin-behavior-phabricator-nav' => '56a1ca03', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '49b73b36', 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => 'eeaa9e5a', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', 'javelin-behavior-phabricator-search-typeahead' => '048330fa', 'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6', 'javelin-behavior-phabricator-tooltips' => '3ee3408b', 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6', 'javelin-behavior-phabricator-transaction-list' => '13c739ea', 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d', 'javelin-behavior-phame-post-preview' => 'be807912', 'javelin-behavior-pholio-mock-edit' => '246dc085', 'javelin-behavior-pholio-mock-view' => 'fbe497e7', 'javelin-behavior-phui-dropdown-menu' => '54733475', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 'javelin-behavior-policy-control' => '7d470398', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-ponder-votebox' => '4e9b766b', 'javelin-behavior-project-boards' => 'ba4fa35c', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', 'javelin-behavior-refresh-csrf' => '7814b593', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8', 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 'javelin-behavior-remarkup-preview' => 'f7379f45', 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-repository-crossreference' => 'bea81850', 'javelin-behavior-scrollbar' => '834a1173', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-on-click' => '4e3e79a6', 'javelin-behavior-slowvote-embed' => '887ad43f', 'javelin-behavior-stripe-payment-form' => '3f5d6dbf', 'javelin-behavior-test-payment-form' => 'fc91ab6c', 'javelin-behavior-time-typeahead' => 'f80d6bf0', 'javelin-behavior-toggle-class' => '5d7c9f33', 'javelin-behavior-typeahead-browse' => '635de1ec', 'javelin-behavior-typeahead-search' => '93d0c9e3', 'javelin-behavior-view-placeholder' => '47830651', 'javelin-behavior-workflow' => '0a3f3021', 'javelin-color' => '7e41274a', 'javelin-cookie' => '62dfea03', 'javelin-diffusion-locate-file-source' => 'b42eddc7', 'javelin-dom' => '147805fa', 'javelin-dynval' => 'f6555212', 'javelin-event' => '85ea0626', 'javelin-fx' => '54b612ba', 'javelin-history' => 'd4505101', 'javelin-install' => '05270951', 'javelin-json' => '69adf288', 'javelin-leader' => '331b1611', 'javelin-magical-init' => '3010e992', 'javelin-mask' => '8a41885b', 'javelin-quicksand' => '4cebc641', 'javelin-reactor' => '2b8de964', 'javelin-reactor-dom' => 'c90a04fc', 'javelin-reactor-node-calmer' => '76f4ebed', 'javelin-reactornode' => '1ad0a787', 'javelin-request' => '94b750d2', 'javelin-resource' => '44959b73', 'javelin-routable' => 'b3e7d692', 'javelin-router' => '29274e2b', 'javelin-scrollbar' => '087e919c', 'javelin-sound' => '949c0fe5', 'javelin-stratcom' => '6c53634d', 'javelin-tokenizer' => 'ab5f468d', 'javelin-typeahead' => '70baed2f', 'javelin-typeahead-composite-source' => '503e17fd', 'javelin-typeahead-normalizer' => 'e6e25838', 'javelin-typeahead-ondemand-source' => '8b3fd187', 'javelin-typeahead-preloaded-source' => '54f314a0', 'javelin-typeahead-source' => '2818f5ce', 'javelin-typeahead-static-source' => '6c0e62fa', 'javelin-uri' => '6eff08aa', 'javelin-util' => '93cc50d6', 'javelin-vector' => '2caa8fb8', 'javelin-view' => '0f764c35', 'javelin-view-html' => 'fe287620', 'javelin-view-interpreter' => 'f829edb3', 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', 'maniphest-report-css' => 'f6931fdf', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', 'multirow-row-manager' => 'b5d57730', 'owners-path-editor' => 'aa1733d0', 'owners-path-editor-css' => '2f00933b', 'paste-css' => '1898e534', 'path-typeahead' => 'f7fc67ec', 'people-profile-css' => '25970776', 'phabricator-action-list-view-css' => 'c5eba19d', 'phabricator-application-launch-view-css' => '95351601', 'phabricator-busy' => '59a7976a', 'phabricator-chatlog-css' => 'd295b020', 'phabricator-content-source-view-css' => '4b8b05d4', 'phabricator-core-css' => 'a76cefc9', - 'phabricator-countdown-css' => '4f02bd98', + 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', 'phabricator-drag-and-drop-file-upload' => '07de8873', 'phabricator-draggable-list' => 'a16ec1c6', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '477359c8', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', 'phabricator-hovercard' => '14ac66f5', 'phabricator-hovercard-view-css' => '1239cd52', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 'phabricator-main-menu-view' => '2f670a96', 'phabricator-nav-view-css' => 'a24cb589', 'phabricator-notification' => 'ccf1cbf8', 'phabricator-notification-css' => '9c279160', 'phabricator-notification-menu-css' => 'f31c0bde', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '6920d200', 'phabricator-remarkup-css' => 'de13dcbe', 'phabricator-search-results-css' => '7dea472c', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-side-menu-view-css' => 'bec2458e', 'phabricator-slowvote-css' => '7c27f0f9', 'phabricator-source-code-view-css' => '5e0178de', 'phabricator-standard-page-view' => '4d176b67', 'phabricator-textareautils' => '5c93c52c', 'phabricator-title' => 'df5e11d2', 'phabricator-tooltip' => '1d298e3a', 'phabricator-ui-example-css' => '528b19de', 'phabricator-uiexample-javelin-view' => 'd4a14807', 'phabricator-uiexample-reactor-button' => 'd19198c8', 'phabricator-uiexample-reactor-checkbox' => '519705ea', 'phabricator-uiexample-reactor-focus' => '40a6a403', 'phabricator-uiexample-reactor-input' => '886fd850', 'phabricator-uiexample-reactor-mouseover' => '47c794d8', 'phabricator-uiexample-reactor-radio' => '988040b4', 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => '57ddcaa2', - 'phame-css' => '3259b53d', + 'phame-css' => 'bb147387', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', 'pholio-inline-comments-css' => '8e545e49', 'phortune-credit-card-form' => '2290aeef', 'phortune-credit-card-form-css' => '8391eb02', 'phortune-css' => '9149f103', 'phrequent-css' => 'ffc185ad', 'phriction-document-css' => 'd1861e06', 'phui-action-panel-css' => '3ee9afd5', - 'phui-badge-view-css' => 'b6218fa8', + 'phui-badge-view-css' => 'f25c3476', 'phui-box-css' => 'a5bb366d', 'phui-button-css' => '16020a60', 'phui-calendar-css' => 'ccabe893', 'phui-calendar-day-css' => 'd1cf6f93', 'phui-calendar-list-css' => 'c1c7f338', 'phui-calendar-month-css' => '476be7e0', 'phui-crumbs-view-css' => 'd842f867', 'phui-document-view-css' => '0267054b', - 'phui-feed-story-css' => 'c7d8113a', + 'phui-feed-story-css' => 'b7b26d23', 'phui-font-icon-base-css' => '3dad2ae3', 'phui-fontkit-css' => 'cb8ae7ad', 'phui-form-css' => 'afdb2c6e', 'phui-form-view-css' => '621b21c5', 'phui-header-view-css' => '55bb32dd', 'phui-icon-view-css' => 'b0a6b1b6', 'phui-image-mask-css' => '5a8b09c8', 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => '5b16bac6', 'phui-inline-comment-view-css' => '9fadd6b8', 'phui-list-view-css' => '125599df', 'phui-object-box-css' => '407eaf5a', - 'phui-object-item-list-view-css' => 'a1b990b7', + 'phui-object-item-list-view-css' => '36ce366c', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-property-list-view-css' => 'aeb09581', + 'phui-property-list-view-css' => '15bbe0b0', 'phui-remarkup-preview-css' => '867f85b3', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '402691cc', 'phui-text-css' => 'cf019f54', 'phui-theme-css' => '6b451f24', - 'phui-timeline-view-css' => 'fc23e7b7', - 'phui-workboard-view-css' => '6a20991a', - 'phui-workpanel-view-css' => '8cebb2b1', + 'phui-timeline-view-css' => 'f1bccf73', + 'phui-workboard-view-css' => '6704d68d', + 'phui-workpanel-view-css' => 'adec7699', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-dropdown-menu' => 'bd4c8dca', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', - 'ponder-comment-table-css' => '865a67e6', - 'ponder-feed-view-css' => 'e62615b6', - 'ponder-post-css' => '9d415218', - 'ponder-vote-css' => 'aea452b0', + 'ponder-view-css' => 'fcd6b398', 'project-icon-css' => '4e3eaa5a', 'raphael-core' => '51ee6b43', 'raphael-g' => '40dde778', 'raphael-g-line' => '40da039e', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', 'releeph-request-typeahead-css' => '667a48ae', 'setup-issue-css' => 'db7e9c40', 'sprite-login-css' => '1ebb9bf9', 'sprite-main-header-css' => 'f07bbb87', 'sprite-menu-css' => '9dd65b92', 'sprite-projects-css' => 'e5ad842a', 'sprite-tokens-css' => '4f399012', 'syntax-highlighting-css' => '9fd11da8', 'tokens-css' => '3d0f239e', 'typeahead-browse-css' => 'd8581d2c', 'unhandled-exception-css' => '4c96257a', ), 'requires' => array( '01774ab2' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-aphlict', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', ), '031cee25' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', 'javelin-behavior-device', 'phabricator-title', ), '037b59eb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-util', 'javelin-vector', 'differential-inline-comment-editor', ), '048330fa' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', 'javelin-typeahead', 'javelin-dom', 'javelin-uri', 'javelin-util', 'javelin-stratcom', 'phabricator-prefab', ), '04875312' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', ), '05270951' => array( 'javelin-util', 'javelin-magical-init', ), '065227cc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', ), '07de8873' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-dom', 'javelin-uri', 'phabricator-file-upload', ), '087e919c' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-router', ), '0f764c35' => array( 'javelin-install', 'javelin-util', ), '13c739ea' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'javelin-uri', 'phabricator-textareautils', ), '147805fa' => array( 'javelin-magical-init', 'javelin-install', 'javelin-util', 'javelin-vector', 'javelin-stratcom', ), '1499a8cb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-history', ), '14ac66f5' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', 'javelin-request', 'javelin-uri', ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', 'javelin-util', 'javelin-reactor-node-calmer', ), '1ae869f2' => array( 'javelin-install', 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), '1d298e3a' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', ), '1def2711' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), '2035b9cb' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'phabricator-phtize', 'changeset-view-manager', ), '21ba5861' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-stratcom', 'conpherence-thread-manager', ), '2290aeef' => array( 'javelin-install', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-util', ), '246dc085' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', 'javelin-quicksand', 'phabricator-phtize', 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), '2818f5ce' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-typeahead-normalizer', ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', ), '29274e2b' => array( 'javelin-install', 'javelin-util', ), '2b228192' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-workflow', 'javelin-json', ), '2b8de964' => array( 'javelin-install', 'javelin-util', ), '2bfa2836' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '2c426492' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-keyboard-shortcut', ), '2caa8fb8' => array( 'javelin-install', 'javelin-event', ), '2f670a96' => array( 'phui-theme-css', ), '331b1611' => array( 'javelin-install', ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-magical-init', ), '3cb0b2fc' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'javelin-uri', ), '3ee3408b' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'phabricator-tooltip', ), '3f5d6dbf' => array( 'javelin-behavior', 'javelin-dom', 'phortune-credit-card-form', ), '40a6a403' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 42126667 => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', ), '44168bad' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-prefab', ), '44959b73' => array( 'javelin-util', 'javelin-uri', 'javelin-install', ), '453c5375' => array( 'javelin-behavior', 'javelin-dom', ), '469c0d9e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', ), '477359c8' => array( 'javelin-install', 'javelin-dom', 'phabricator-notification', ), 47830651 => array( 'javelin-behavior', 'javelin-dom', 'javelin-view-renderer', 'javelin-install', ), '47c794d8' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 48086888 => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', ), '49b73b36' => array( 'javelin-behavior', 'javelin-dom', 'javelin-request', 'javelin-util', ), '4c95d29e' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-json', 'javelin-stratcom', 'phabricator-shaped-request', ), '4cebc641' => array( 'javelin-install', ), '4e3e79a6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '4e9b766b' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-request', ), '4fdb476d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '503e17fd' => array( 'javelin-install', 'javelin-typeahead-source', 'javelin-util', ), '519705ea' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), - 52684226 => array( - 'multirow-row-manager', - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-json', - 'phabricator-prefab', - ), '5359e785' => array( 'javelin-install', 'javelin-util', 'javelin-websocket', 'javelin-leader', 'javelin-json', ), 54733475 => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phuix-dropdown-menu', ), '54b612ba' => array( 'javelin-color', 'javelin-install', 'javelin-util', ), '54f314a0' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '558829c2' => array( 'javelin-stratcom', 'javelin-behavior', 'javelin-vector', 'javelin-dom', ), '56a1ca03' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-dom', 'javelin-magical-init', 'javelin-vector', 'javelin-request', 'javelin-util', ), 58562350 => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-workflow', 'javelin-router', 'javelin-behavior-device', 'javelin-vector', ), '59a7976a' => array( 'javelin-install', 'javelin-dom', 'javelin-fx', ), '59b251eb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', ), '5ab1a46a' => array( 'phui-fontkit-css', ), '5b2e3e2b' => array( 'javelin-stratcom', 'javelin-request', 'javelin-dom', 'javelin-vector', 'javelin-install', 'javelin-util', 'javelin-mask', 'javelin-uri', 'javelin-routable', ), '5c54cbf3' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '5c93c52c' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', ), '5d7c9f33' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '5e9f347c' => array( 'javelin-behavior', 'multirow-row-manager', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'javelin-json', ), '5fefb143' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', 'javelin-stratcom', ), 60479091 => array( 'phabricator-busy', 'javelin-behavior', ), '60821bc7' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), '6153c708' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-workflow', ), '61cbc29a' => array( 'javelin-magical-init', 'javelin-util', ), '62dfea03' => array( 'javelin-install', 'javelin-util', ), '635de1ec' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), '665cf6ac' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', ), '6882e80a' => array( 'javelin-dom', ), '6920d200' => 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', ), '69adf288' => array( 'javelin-install', ), '6c0e62fa' => array( 'javelin-install', 'javelin-typeahead-source', ), '6c2b09a2' => array( 'javelin-install', 'javelin-util', ), '6c53634d' => array( 'javelin-install', 'javelin-event', 'javelin-util', 'javelin-magical-init', ), '6d3e1947' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', 'javelin-dom', 'javelin-typeahead', 'javelin-uri', ), '6d49590e' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-drag-and-drop-file-upload', 'phabricator-textareautils', ), '6eff08aa' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', ), '70baed2f' => array( 'javelin-install', 'javelin-dom', 'javelin-vector', 'javelin-util', ), 71237763 => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '7319e029' => array( 'javelin-behavior', 'javelin-dom', ), '73d09eef' => array( 'javelin-behavior', 'javelin-vector', 'javelin-dom', ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), '76f4ebed' => array( 'javelin-install', 'javelin-reactor', 'javelin-util', ), '7814b593' => array( 'javelin-request', 'javelin-behavior', 'javelin-dom', 'javelin-router', 'javelin-util', 'phabricator-busy', ), '782ab6e7' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-prefab', 'multirow-row-manager', 'javelin-json', ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', ), '7a68dda3' => array( 'owners-path-editor', 'javelin-behavior', ), '7b98d7c5' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-util', ), '7cbe244b' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-router', ), '7d470398' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'javelin-workflow', ), '7e41274a' => array( 'javelin-install', ), '7ebaeed3' => array( 'herald-rule-editor', 'javelin-behavior', ), '7ee2b591' => array( 'javelin-behavior', 'javelin-history', ), 82439934 => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', ), '85ea0626' => array( 'javelin-install', ), '85ee8ce6' => array( 'aphront-dialog-view-css', ), '8694b1df' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-tooltip', 'changeset-view-manager', ), '88236f00' => array( 'javelin-behavior', 'phabricator-keyboard-shortcut', 'javelin-stratcom', ), '886fd850' => array( 'javelin-install', 'javelin-reactor-dom', 'javelin-view-html', 'javelin-view-interpreter', 'javelin-view-renderer', ), '887ad43f' => array( 'javelin-behavior', 'javelin-request', 'javelin-stratcom', 'javelin-dom', ), '88f0c5b3' => array( 'javelin-behavior', 'javelin-dom', 'javelin-vector', ), '8a41885b' => array( 'javelin-install', 'javelin-dom', ), '8b3fd187' => array( 'javelin-install', 'javelin-util', 'javelin-request', 'javelin-typeahead-source', ), '8ce821c5' => array( 'phabricator-notification', 'javelin-stratcom', 'javelin-behavior', ), '8cf6d262' => array( 'javelin-install', 'javelin-dom', 'javelin-util', ), '8ef9ab58' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), '9007c197' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), + '91a6031b' => array( + 'multirow-row-manager', + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-json', + 'phabricator-prefab', + ), '93d0c9e3' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', ), '9414ff18' => array( 'javelin-behavior', 'javelin-resource', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', ), '949c0fe5' => array( 'javelin-install', ), '94b750d2' => array( 'javelin-install', 'javelin-stratcom', 'javelin-util', 'javelin-behavior', 'javelin-json', 'javelin-dom', 'javelin-resource', 'javelin-routable', ), '988040b4' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), '9f36c42d' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), 'a0b57eb8' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-keyboard-shortcut', ), 'a155550f' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 'a16ec1c6' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'javelin-vector', 'javelin-magical-init', ), 'a205cf28' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', 'javelin-install', ), 'a464fe03' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-notification', ), 'a80d0378' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'a8458711' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-util', 'phabricator-notification', 'javelin-behavior-device', 'phuix-dropdown-menu', 'phuix-action-list-view', 'phuix-action-view', 'conpherence-thread-manager', ), 'a8d8459d' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'a8da01f0' => array( 'javelin-behavior', 'javelin-uri', 'phabricator-keyboard-shortcut', ), 'a9f88de2' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-workflow', 'javelin-fx', 'javelin-util', ), 'aa1733d0' => array( 'multirow-row-manager', 'javelin-install', 'path-typeahead', 'javelin-dom', 'javelin-util', 'phabricator-prefab', ), 'ab5f468d' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', ), 'b064af76' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-request', 'javelin-util', 'phabricator-shaped-request', ), 'b1f0ccee' => array( 'javelin-install', 'javelin-dom', 'javelin-reactor-dom', ), 'b23b49e6' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-request', 'phabricator-shaped-request', ), 'b2b4fbaf' => array( 'javelin-behavior', 'javelin-dom', 'javelin-uri', 'javelin-request', ), 'b3a4b884' => array( 'javelin-behavior', 'phabricator-prefab', ), 'b3e7d692' => array( 'javelin-install', ), 'b42eddc7' => array( 'javelin-install', 'javelin-dom', 'javelin-typeahead-preloaded-source', 'javelin-util', ), 'b5c256b8' => array( 'javelin-install', 'javelin-dom', ), 'b5d57730' => array( 'javelin-install', 'javelin-stratcom', 'javelin-dom', 'javelin-util', ), 'b6993408' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-json', 'phabricator-draggable-list', ), 'b6b0d1bb' => array( 'phui-inline-comment-view-css', ), 'ba4fa35c' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', 'phabricator-draggable-list', ), 'bd4c8dca' => array( 'javelin-install', 'javelin-util', 'javelin-dom', 'javelin-vector', 'javelin-stratcom', ), 'bdaf4d04' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'javelin-request', ), 'be807912' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'bea81850' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-uri', ), 'c1700f6f' => array( 'javelin-install', 'javelin-util', 'javelin-stratcom', 'javelin-dom', 'javelin-vector', ), 'c72aa091' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'javelin-behavior-device', 'javelin-scrollbar', 'javelin-quicksand', 'phabricator-keyboard-shortcut', 'conpherence-thread-manager', ), 'c8e57404' => array( 'javelin-behavior', 'javelin-dom', 'javelin-uri', 'javelin-mask', 'phabricator-drag-and-drop-file-upload', ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', 'javelin-reactor', 'javelin-reactornode', 'javelin-install', 'javelin-util', ), 'ca3f91eb' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', 'phabricator-phtize', ), 'ccf1cbf8' => array( 'javelin-install', 'javelin-dom', 'javelin-stratcom', 'javelin-util', 'phabricator-notification-css', ), 'cf86d16a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-workflow', 'phabricator-drag-and-drop-file-upload', ), 'd19198c8' => array( 'javelin-install', 'javelin-dom', 'javelin-util', 'javelin-dynval', 'javelin-reactor-dom', ), 'd254d646' => array( 'javelin-util', ), 'd3782c93' => 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', ), 'd4505101' => array( 'javelin-stratcom', 'javelin-install', 'javelin-uri', 'javelin-util', ), 'd4a14807' => array( 'javelin-install', 'javelin-dom', 'javelin-view', ), 'd4c87bf4' => array( 'javelin-dom', 'javelin-util', 'javelin-stratcom', 'javelin-install', 'javelin-request', 'javelin-workflow', ), 'd4eecc63' => array( 'javelin-behavior', 'javelin-dom', 'javelin-stratcom', ), 'd75709e6' => array( 'javelin-behavior', 'javelin-workflow', 'javelin-json', 'javelin-dom', 'phabricator-keyboard-shortcut', ), 'd835b03a' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'dbbf48b6' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-busy', ), 'de2e896f' => array( 'javelin-behavior', 'javelin-dom', 'javelin-typeahead', 'javelin-typeahead-ondemand-source', 'javelin-dom', ), 'df5e11d2' => array( 'javelin-install', ), 'e10f8e18' => array( 'javelin-behavior', 'javelin-dom', 'phabricator-prefab', ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'e1ff79b1' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', ), 'e292eaf4' => array( 'javelin-install', ), 'e379b58e' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-vector', 'javelin-dom', 'javelin-uri', ), 'e4cc26b3' => array( 'javelin-behavior', 'javelin-dom', ), 'e5822781' => array( 'javelin-behavior', 'javelin-dom', 'javelin-json', 'javelin-workflow', 'javelin-magical-init', ), 'e6e25838' => array( 'javelin-install', ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-workflow', 'javelin-dom', 'phabricator-draggable-list', ), 'ea681761' => array( 'javelin-behavior', 'javelin-aphlict', 'phabricator-phtize', 'javelin-dom', ), 'edd1ba66' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-uri', 'phabricator-notification', ), 'edf8a145' => array( 'javelin-behavior', 'javelin-uri', ), 'eeaa9e5a' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'phabricator-phtize', 'phabricator-textareautils', 'javelin-workflow', 'javelin-vector', ), 'efe49472' => array( 'javelin-install', 'javelin-util', ), 'f24a53cb' => array( 'phui-fontkit-css', ), 'f36e01af' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', 'javelin-vector', 'phabricator-hovercard', ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-util', 'javelin-dom', 'javelin-request', 'phabricator-keyboard-shortcut', ), 'f6555212' => array( 'javelin-install', 'javelin-reactornode', 'javelin-util', 'javelin-reactor', ), 'f7379f45' => array( 'javelin-behavior', 'javelin-dom', 'javelin-util', 'phabricator-shaped-request', ), 'f7fc67ec' => array( 'javelin-install', 'javelin-typeahead', 'javelin-dom', 'javelin-request', 'javelin-typeahead-ondemand-source', 'javelin-util', ), 'f80d6bf0' => array( 'javelin-behavior', 'javelin-util', 'javelin-dom', 'javelin-stratcom', 'javelin-vector', 'javelin-typeahead-static-source', ), 'f829edb3' => array( 'javelin-view', 'javelin-install', 'javelin-dom', ), 'f8ba29d7' => array( 'javelin-behavior', 'javelin-stratcom', 'javelin-dom', 'javelin-mask', 'javelin-util', 'phabricator-busy', ), 'fa0f4fc2' => array( 'javelin-behavior', 'javelin-dom', 'javelin-vector', 'javelin-magical-init', ), 'fb20ac8d' => 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', ), 'fbe497e7' => 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', ), 'fc91ab6c' => array( 'javelin-behavior', 'javelin-dom', 'phortune-credit-card-form', ), 'fe287620' => array( 'javelin-install', 'javelin-dom', 'javelin-view-visitor', 'javelin-util', ), ), 'packages' => array( 'core.pkg.css' => array( 'phabricator-core-css', 'phabricator-zindex-css', 'phui-button-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', 'phabricator-remarkup-css', 'syntax-highlighting-css', 'phui-pager-css', 'aphront-tooltip-css', 'phabricator-flag-css', 'phui-info-view-css', 'sprite-menu-css', 'phabricator-main-menu-view', 'phabricator-notification-css', 'phabricator-notification-menu-css', 'lightbox-attachment-css', 'phui-header-view-css', 'phabricator-filetree-view-css', 'phabricator-nav-view-css', 'phabricator-side-menu-view-css', 'phui-crumbs-view-css', 'phui-object-item-list-view-css', 'global-drag-and-drop-css', 'phui-spacing-css', 'phui-form-css', 'phui-icon-view-css', 'phabricator-application-launch-view-css', 'phabricator-action-list-view-css', 'phui-property-list-view-css', 'phui-tag-view-css', 'phui-list-view-css', 'font-fontawesome', 'phui-font-icon-base-css', 'phui-box-css', 'phui-object-box-css', 'phui-timeline-view-css', 'sprite-tokens-css', 'tokens-css', 'phui-status-list-view-css', 'phui-feed-story-css', 'phabricator-feed-css', 'phabricator-dashboard-css', 'aphront-multi-column-view-css', 'conpherence-durable-column-view', ), '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', '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-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', 'phabricator-hovercard', 'javelin-behavior-phabricator-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-scrollbar', 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', 'conpherence-thread-manager', ), 'darkconsole.pkg.js' => array( 'javelin-behavior-dark-console', 'javelin-behavior-error-log', ), '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', ), 'differential.pkg.js' => array( 'phabricator-drag-and-drop-file-upload', 'phabricator-shaped-request', 'javelin-behavior-differential-feedback-preview', 'javelin-behavior-differential-edit-inline-comments', 'javelin-behavior-differential-populate', 'javelin-behavior-differential-diff-radios', 'javelin-behavior-differential-comment-jump', 'javelin-behavior-differential-add-reviewers-and-ccs', 'javelin-behavior-differential-keyboard-navigation', 'javelin-behavior-aphront-drag-and-drop-textarea', 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', 'javelin-behavior-load-blame', 'differential-inline-comment-editor', 'javelin-behavior-differential-dropdown-menus', 'javelin-behavior-differential-toggle-files', 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', 'changeset-view-manager', ), '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-transaction-controls', 'javelin-behavior-maniphest-transaction-preview', 'javelin-behavior-maniphest-transaction-expand', 'javelin-behavior-maniphest-subpriority-editor', 'javelin-behavior-maniphest-list-editor', ), ), ); diff --git a/resources/sql/autopatches/20150725.badges.mailkey.1.sql b/resources/sql/autopatches/20150725.badges.mailkey.1.sql new file mode 100644 index 0000000000..5219a759b3 --- /dev/null +++ b/resources/sql/autopatches/20150725.badges.mailkey.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_badges.badges_badge + ADD mailKey binary(20) NOT NULL; diff --git a/resources/sql/autopatches/20150725.badges.mailkey.2.php b/resources/sql/autopatches/20150725.badges.mailkey.2.php new file mode 100644 index 0000000000..8231492478 --- /dev/null +++ b/resources/sql/autopatches/20150725.badges.mailkey.2.php @@ -0,0 +1,18 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $badge) { + $id = $badge->getID(); + + echo pht('Adding mail key for badge %d...', $id); + echo "\n"; + + queryfx( + $conn_w, + 'UPDATE %T SET mailKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); +} diff --git a/resources/sql/autopatches/20150725.badges.viewpolicy.3.sql b/resources/sql/autopatches/20150725.badges.viewpolicy.3.sql new file mode 100644 index 0000000000..a5933f4ade --- /dev/null +++ b/resources/sql/autopatches/20150725.badges.viewpolicy.3.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_badges.badges_badge + DROP COLUMN viewPolicy; diff --git a/resources/sql/autopatches/20150725.countdown.mailkey.1.sql b/resources/sql/autopatches/20150725.countdown.mailkey.1.sql new file mode 100644 index 0000000000..c53441e1cb --- /dev/null +++ b/resources/sql/autopatches/20150725.countdown.mailkey.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_countdown.countdown + ADD mailKey binary(20) NOT NULL; diff --git a/resources/sql/autopatches/20150725.countdown.mailkey.2.php b/resources/sql/autopatches/20150725.countdown.mailkey.2.php new file mode 100644 index 0000000000..c07a763942 --- /dev/null +++ b/resources/sql/autopatches/20150725.countdown.mailkey.2.php @@ -0,0 +1,18 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $countdown) { + $id = $countdown->getID(); + + echo pht('Adding mail key for countdown %d...', $id); + echo "\n"; + + queryfx( + $conn_w, + 'UPDATE %T SET mailKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); +} diff --git a/resources/sql/autopatches/20150725.slowvote.mailkey.1.sql b/resources/sql/autopatches/20150725.slowvote.mailkey.1.sql new file mode 100644 index 0000000000..00591afa56 --- /dev/null +++ b/resources/sql/autopatches/20150725.slowvote.mailkey.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_slowvote.slowvote_poll + ADD mailKey binary(20) NOT NULL; diff --git a/resources/sql/autopatches/20150725.slowvote.mailkey.2.php b/resources/sql/autopatches/20150725.slowvote.mailkey.2.php new file mode 100644 index 0000000000..fa6b2035df --- /dev/null +++ b/resources/sql/autopatches/20150725.slowvote.mailkey.2.php @@ -0,0 +1,18 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $slowvote) { + $id = $slowvote->getID(); + + echo pht('Adding mail key for Slowvote %d...', $id); + echo "\n"; + + queryfx( + $conn_w, + 'UPDATE %T SET mailKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); +} diff --git a/resources/sql/autopatches/20150730.herald.1.sql b/resources/sql/autopatches/20150730.herald.1.sql new file mode 100644 index 0000000000..e6e097f82c --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.1.sql @@ -0,0 +1,27 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'subscribers.add' + WHERE r.ruleType != 'personal' + AND a.action = 'addcc'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'subscribers.self.add' + WHERE r.ruleType = 'personal' + AND a.action = 'addcc'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'subscribers.remove' + WHERE r.ruleType != 'personal' + AND a.action = 'remcc'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'subscribers.self.remove' + WHERE r.ruleType = 'personal' + AND a.action = 'remcc'; diff --git a/resources/sql/autopatches/20150730.herald.2.sql b/resources/sql/autopatches/20150730.herald.2.sql new file mode 100644 index 0000000000..3d0b5a0a05 --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.2.sql @@ -0,0 +1,13 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'email.other' + WHERE r.ruleType != 'personal' + AND a.action = 'email'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'email.self' + WHERE r.ruleType = 'personal' + AND a.action = 'email'; diff --git a/resources/sql/autopatches/20150730.herald.3.sql b/resources/sql/autopatches/20150730.herald.3.sql new file mode 100644 index 0000000000..e9fcbff59b --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.3.sql @@ -0,0 +1,11 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'projects.add' + WHERE a.action = 'addprojects'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'projects.remove' + WHERE a.action = 'removeprojects'; diff --git a/resources/sql/autopatches/20150730.herald.4.sql b/resources/sql/autopatches/20150730.herald.4.sql new file mode 100644 index 0000000000..1f5c35a851 --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.4.sql @@ -0,0 +1,13 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'maniphest.assign.other' + WHERE r.ruleType != 'personal' + AND a.action = 'assigntask'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'maniphest.assign.self' + WHERE r.ruleType = 'personal' + AND a.action = 'assigntask'; diff --git a/resources/sql/autopatches/20150730.herald.5.sql b/resources/sql/autopatches/20150730.herald.5.sql new file mode 100644 index 0000000000..84c9eea3c5 --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.5.sql @@ -0,0 +1,27 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'differential.reviewers.blocking' + WHERE r.ruleType != 'personal' + AND a.action = 'addreviewers'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'differential.reviewers.self.blocking' + WHERE r.ruleType = 'personal' + AND a.action = 'addreviewers'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'differential.reviewers.add' + WHERE r.ruleType != 'personal' + AND a.action = 'addblockingreviewers'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'differential.reviewers.self.add' + WHERE r.ruleType = 'personal' + AND a.action = 'addblockingreviewers'; diff --git a/resources/sql/autopatches/20150730.herald.6.sql b/resources/sql/autopatches/20150730.herald.6.sql new file mode 100644 index 0000000000..08188c6d4f --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.6.sql @@ -0,0 +1,6 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'legalpad.require' + WHERE r.ruleType != 'personal' + AND a.action = 'signature'; diff --git a/resources/sql/autopatches/20150730.herald.7.sql b/resources/sql/autopatches/20150730.herald.7.sql new file mode 100644 index 0000000000..403a90ac26 --- /dev/null +++ b/resources/sql/autopatches/20150730.herald.7.sql @@ -0,0 +1,6 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'harbormaster.build' + WHERE r.ruleType != 'personal' + AND a.action = 'applybuildplans'; diff --git a/resources/sql/autopatches/20150803.herald.1.sql b/resources/sql/autopatches/20150803.herald.1.sql new file mode 100644 index 0000000000..21f462b8c8 --- /dev/null +++ b/resources/sql/autopatches/20150803.herald.1.sql @@ -0,0 +1,13 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'diffusion.auditors.add' + WHERE r.ruleType != 'personal' + AND a.action = 'audit'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'diffusion.auditors.self.add' + WHERE r.ruleType = 'personal' + AND a.action = 'audit'; diff --git a/resources/sql/autopatches/20150803.herald.2.sql b/resources/sql/autopatches/20150803.herald.2.sql new file mode 100644 index 0000000000..4d48bc03b6 --- /dev/null +++ b/resources/sql/autopatches/20150803.herald.2.sql @@ -0,0 +1,13 @@ +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'diffusion.block' + WHERE r.contentType != 'differential.diff' + AND a.action = 'block'; + +UPDATE {$NAMESPACE}_herald.herald_action a + JOIN {$NAMESPACE}_herald.herald_rule r + ON a.ruleID = r.id + SET a.action = 'differential.block' + WHERE r.contentType = 'differential.diff' + AND a.action = 'block'; diff --git a/resources/sql/autopatches/20150804.ponder.answer.mailkey.1.sql b/resources/sql/autopatches/20150804.ponder.answer.mailkey.1.sql new file mode 100644 index 0000000000..63ba3494f9 --- /dev/null +++ b/resources/sql/autopatches/20150804.ponder.answer.mailkey.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_ponder.ponder_answer + ADD mailKey binary(20) NOT NULL; diff --git a/resources/sql/autopatches/20150804.ponder.answer.mailkey.2.php b/resources/sql/autopatches/20150804.ponder.answer.mailkey.2.php new file mode 100644 index 0000000000..e643a5dab5 --- /dev/null +++ b/resources/sql/autopatches/20150804.ponder.answer.mailkey.2.php @@ -0,0 +1,18 @@ +establishConnection('w'); +$iterator = new LiskMigrationIterator($table); +foreach ($iterator as $answer) { + $id = $answer->getID(); + + echo pht('Adding mail key for Answer %d...', $id); + echo "\n"; + + queryfx( + $conn_w, + 'UPDATE %T SET mailKey = %s WHERE id = %d', + $table->getTableName(), + Filesystem::readRandomCharacters(20), + $id); +} diff --git a/resources/sql/autopatches/20150804.ponder.question.1.sql b/resources/sql/autopatches/20150804.ponder.question.1.sql new file mode 100644 index 0000000000..8ea3b2c34c --- /dev/null +++ b/resources/sql/autopatches/20150804.ponder.question.1.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_ponder.ponder_question + ADD editPolicy VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_ponder.ponder_question + ADD viewPolicy VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20150804.ponder.question.2.sql b/resources/sql/autopatches/20150804.ponder.question.2.sql new file mode 100644 index 0000000000..ddca10f753 --- /dev/null +++ b/resources/sql/autopatches/20150804.ponder.question.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_ponder.ponder_question + SET editPolicy = authorPHID WHERE editPolicy = ''; diff --git a/resources/sql/autopatches/20150804.ponder.question.3.sql b/resources/sql/autopatches/20150804.ponder.question.3.sql new file mode 100644 index 0000000000..0a359453ea --- /dev/null +++ b/resources/sql/autopatches/20150804.ponder.question.3.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_ponder.ponder_question + SET viewPolicy = 'users' WHERE viewPolicy = ''; diff --git a/resources/sql/autopatches/20150804.ponder.spaces.4.sql b/resources/sql/autopatches/20150804.ponder.spaces.4.sql new file mode 100644 index 0000000000..4282b8a509 --- /dev/null +++ b/resources/sql/autopatches/20150804.ponder.spaces.4.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_ponder.ponder_question + ADD spacePHID VARBINARY(64); diff --git a/resources/sql/autopatches/20150805.paste.status.1.sql b/resources/sql/autopatches/20150805.paste.status.1.sql new file mode 100644 index 0000000000..c9b98bf58c --- /dev/null +++ b/resources/sql/autopatches/20150805.paste.status.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pastebin.pastebin_paste + ADD status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150805.paste.status.2.sql b/resources/sql/autopatches/20150805.paste.status.2.sql new file mode 100644 index 0000000000..be2fe2c485 --- /dev/null +++ b/resources/sql/autopatches/20150805.paste.status.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_pastebin.pastebin_paste + SET status = 'active' WHERE status = ''; diff --git a/scripts/symbols/import_repository_symbols.php b/scripts/symbols/import_repository_symbols.php index 94cf601a34..2ac2b3c8a9 100755 --- a/scripts/symbols/import_repository_symbols.php +++ b/scripts/symbols/import_repository_symbols.php @@ -1,229 +1,229 @@ #!/usr/bin/env php setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'no-purge', 'help' => pht( 'Do not clear all symbols for this repository before '. 'uploading new symbols. Useful for incremental updating.'), ), array( 'name' => 'ignore-errors', 'help' => pht( "If a line can't be parsed, ignore that line and ". "continue instead of exiting."), ), array( 'name' => 'max-transaction', 'param' => 'num-syms', 'default' => '100000', 'help' => pht( 'Maximum number of symbols that should '. 'be part of a single transaction.'), ), array( 'name' => 'callsign', 'wildcard' => true, ), )); $callsigns = $args->getArg('callsign'); if (count($callsigns) !== 1) { $args->printHelpAndExit(); } $callsign = head($callsigns); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withCallsigns($callsigns) ->executeOne(); if (!$repository) { echo pht("Repository '%s' does not exist.", $callsign); exit(1); } if (!function_exists('posix_isatty') || posix_isatty(STDIN)) { echo pht('Parsing input from stdin...'), "\n"; } $input = file_get_contents('php://stdin'); $input = trim($input); $input = explode("\n", $input); function commit_symbols( array $symbols, PhabricatorRepository $repository, $no_purge) { echo pht('Looking up path IDs...'), "\n"; $path_map = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( ipull($symbols, 'path')); $symbol = new PhabricatorRepositorySymbol(); $conn_w = $symbol->establishConnection('w'); echo pht('Preparing queries...'), "\n"; $sql = array(); foreach ($symbols as $dict) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %s, %s, %d, %d)', $repository->getPHID(), $dict['ctxt'], $dict['name'], $dict['type'], $dict['lang'], $dict['line'], $path_map[$dict['path']]); } if (!$no_purge) { echo pht('Purging old symbols...'), "\n"; queryfx( $conn_w, 'DELETE FROM %T WHERE repositoryPHID = %s', $symbol->getTableName(), $repository->getPHID()); } echo pht('Loading %s symbols...', new PhutilNumber(count($sql))), "\n"; foreach (array_chunk($sql, 128) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryPHID, symbolContext, symbolName, symbolType, symbolLanguage, lineNumber, pathID) VALUES %Q', $symbol->getTableName(), implode(', ', $chunk)); } } function check_string_value($value, $field_name, $line_no, $max_length) { if (strlen($value) > $max_length) { throw new Exception( pht( "%s '%s' defined on line #%d is too long, ". "maximum %s length is %d characters.", $field_name, $value, $line_no, $field_name, $max_length)); } if (!phutil_is_utf8_with_only_bmp_characters($value)) { throw new Exception( pht( "%s '%s' defined on line #%d is not a valid ". "UTF-8 string, it should contain only UTF-8 characters.", $field_name, $value, $line_no)); } } $no_purge = $args->getArg('no-purge'); $symbols = array(); foreach ($input as $key => $line) { try { $line_no = $key + 1; $matches = null; $ok = preg_match( '/^((?P[^ ]+)? )?(?P[^ ]+) (?P[^ ]+) '. '(?P[^ ]+) (?P\d+) (?P.*)$/', $line, $matches); if (!$ok) { throw new Exception( pht( "Line #%d of input is invalid. Expected five or six space-delimited ". "fields: maybe symbol context, symbol name, symbol type, symbol ". "language, line number, path. For example:\n\n%s\n\n". "Actual line was:\n\n%s", $line_no, 'idx function php 13 /path/to/some/file.php', $line)); } if (empty($matches['context'])) { $matches['context'] = ''; } $context = $matches['context']; $name = $matches['name']; $type = $matches['type']; $lang = $matches['lang']; $line_number = $matches['line']; $path = $matches['path']; check_string_value($context, pht('Symbol context'), $line_no, 128); check_string_value($name, pht('Symbol name'), $line_no, 128); check_string_value($type, pht('Symbol type'), $line_no, 12); check_string_value($lang, pht('Symbol language'), $line_no, 32); check_string_value($path, pht('Path'), $line_no, 512); if (!strlen($path) || $path[0] != '/') { throw new Exception( pht( "Path '%s' defined on line #%d is invalid. Paths should begin with ". "'%s' and specify a path from the root of the project, like '%s'.", $path, $line_no, '/', '/src/utils/utils.php')); } $symbols[] = array( 'ctxt' => $context, 'name' => $name, 'type' => $type, 'lang' => $lang, 'line' => $line_number, 'path' => $path, ); } catch (Exception $e) { if ($args->getArg('ignore-errors')) { continue; } else { throw $e; } } - if (count ($symbols) >= $args->getArg('max-transaction')) { + if (count($symbols) >= $args->getArg('max-transaction')) { try { echo pht( "Committing %s symbols...\n", new PhutilNumber($args->getArg('max-transaction'))); commit_symbols($symbols, $repository, $no_purge); $no_purge = true; unset($symbols); $symbols = array(); } catch (Exception $e) { if ($args->getArg('ignore-errors')) { continue; } else { throw $e; } } } } if (count($symbols)) { commit_symbols($symbols, $repository, $no_purge); } echo pht('Done.')."\n"; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 64bad75915..0f00bd7430 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,7667 +1,7774 @@ 2, 'class' => array( 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', 'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php', 'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php', 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', 'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php', 'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php', 'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php', 'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php', 'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php', 'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php', 'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php', 'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php', 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', 'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php', 'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php', 'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php', 'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php', 'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php', 'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php', 'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php', 'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.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', 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', 'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php', 'AlmanacQueryDevicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php', 'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php', 'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php', 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', 'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php', 'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php', 'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php', 'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php', 'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php', 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', 'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php', 'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php', 'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php', 'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.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', 'AphrontCSRFException' => 'aphront/exception/AphrontCSRFException.php', 'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php', 'AphrontController' => 'aphront/AphrontController.php', 'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php', 'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php', 'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php', 'AphrontDialogView' => 'view/AphrontDialogView.php', 'AphrontException' => 'aphront/exception/AphrontException.php', 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', 'AphrontFormChooseButtonControl' => 'view/form/control/AphrontFormChooseButtonControl.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', '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', 'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php', 'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php', 'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.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', 'AphrontMoreView' => 'view/layout/AphrontMoreView.php', 'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php', 'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php', 'AphrontNullView' => 'view/AphrontNullView.php', 'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php', 'AphrontPageView' => 'view/page/AphrontPageView.php', 'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php', 'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.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', 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', 'AphrontSite' => 'aphront/site/AphrontSite.php', 'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php', 'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php', 'AphrontTableView' => 'view/control/AphrontTableView.php', 'AphrontTagView' => 'view/AphrontTagView.php', 'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php', - 'AphrontTwoColumnView' => 'view/layout/AphrontTwoColumnView.php', 'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php', 'AphrontURIMapper' => 'aphront/AphrontURIMapper.php', 'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php', 'AphrontUsageException' => 'aphront/exception/AphrontUsageException.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', 'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php', 'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php', 'CelerityAPI' => 'applications/celerity/CelerityAPI.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', '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', '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', 'ConduitCall' => 'applications/conduit/call/ConduitCall.php', 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', 'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php', 'ConduitConnectionGarbageCollector' => 'applications/conduit/garbagecollector/ConduitConnectionGarbageCollector.php', 'ConduitDeprecatedCallSetupCheck' => 'applications/conduit/check/ConduitDeprecatedCallSetupCheck.php', 'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php', 'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php', 'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php', 'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php', 'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php', 'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php', 'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php', 'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php', 'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php', 'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php', 'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php', 'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php', 'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php', 'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php', 'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', 'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php', 'ConpherenceCreateThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceCreateThreadMailReceiver.php', 'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', 'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php', 'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php', 'ConpherenceFileWidgetView' => 'applications/conpherence/view/ConpherenceFileWidgetView.php', 'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php', 'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php', 'ConpherenceHovercardEventListener' => 'applications/conpherence/events/ConpherenceHovercardEventListener.php', 'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.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', 'ConpherenceNewRoomController' => 'applications/conpherence/controller/ConpherenceNewRoomController.php', 'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php', 'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', 'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php', 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', 'ConpherencePeopleWidgetView' => 'applications/conpherence/view/ConpherencePeopleWidgetView.php', 'ConpherencePicCropControl' => 'applications/conpherence/view/ConpherencePicCropControl.php', 'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php', 'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php', 'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php', 'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php', 'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php', 'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php', 'ConpherenceSettings' => 'applications/conpherence/constants/ConpherenceSettings.php', 'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php', 'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php', 'ConpherenceThreadIndexer' => 'applications/conpherence/search/ConpherenceThreadIndexer.php', 'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php', 'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php', 'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php', 'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', 'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php', 'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.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', 'ConpherenceWidgetConfigConstants' => 'applications/conpherence/constants/ConpherenceWidgetConfigConstants.php', 'ConpherenceWidgetController' => 'applications/conpherence/controller/ConpherenceWidgetController.php', 'ConpherenceWidgetView' => 'applications/conpherence/view/ConpherenceWidgetView.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', 'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php', 'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php', 'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php', 'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php', 'DatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DatabaseConfigurationProvider.php', 'DefaultDatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php', 'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php', 'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php', 'DifferentialActionMenuEventListener' => 'applications/differential/event/DifferentialActionMenuEventListener.php', 'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php', 'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php', 'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php', 'DifferentialApplyPatchField' => 'applications/differential/customfield/DifferentialApplyPatchField.php', 'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php', 'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php', 'DifferentialAuthorField' => 'applications/differential/customfield/DifferentialAuthorField.php', 'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php', + 'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php', 'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.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', 'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php', 'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php', 'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.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', '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', 'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php', 'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php', 'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php', 'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php', 'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php', 'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php', 'DifferentialConflictsField' => 'applications/differential/customfield/DifferentialConflictsField.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', 'DifferentialDependenciesField' => 'applications/differential/customfield/DifferentialDependenciesField.php', 'DifferentialDependsOnField' => 'applications/differential/customfield/DifferentialDependsOnField.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', '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', 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/DifferentialDiffTableOfContentsView.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', 'DifferentialDraft' => 'applications/differential/storage/DifferentialDraft.php', 'DifferentialEditPolicyField' => 'applications/differential/customfield/DifferentialEditPolicyField.php', 'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php', 'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php', 'DifferentialFindConduitAPIMethod' => 'applications/differential/conduit/DifferentialFindConduitAPIMethod.php', 'DifferentialFinishPostponedLintersConduitAPIMethod' => 'applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.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', 'DifferentialGitHubLandingStrategy' => 'applications/differential/landing/DifferentialGitHubLandingStrategy.php', 'DifferentialGitSVNIDField' => 'applications/differential/customfield/DifferentialGitSVNIDField.php', 'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php', 'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php', 'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php', 'DifferentialHostedGitLandingStrategy' => 'applications/differential/landing/DifferentialHostedGitLandingStrategy.php', 'DifferentialHostedMercurialLandingStrategy' => 'applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php', 'DifferentialHovercardEventListener' => 'applications/differential/event/DifferentialHovercardEventListener.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', 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php', 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php', 'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php', 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php', 'DifferentialLegacyHunk' => 'applications/differential/storage/DifferentialLegacyHunk.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', 'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php', 'DifferentialModernHunk' => 'applications/differential/storage/DifferentialModernHunk.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', 'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php', 'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php', 'DifferentialProjectsField' => 'applications/differential/customfield/DifferentialProjectsField.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', 'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php', 'DifferentialReviewedByField' => 'applications/differential/customfield/DifferentialReviewedByField.php', 'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.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', 'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php', + 'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php', 'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php', 'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php', 'DifferentialRevisionAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialRevisionAffectedFilesHeraldField.php', 'DifferentialRevisionAuthorHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorHeraldField.php', 'DifferentialRevisionAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorProjectsHeraldField.php', 'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.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', 'DifferentialRevisionDetailView' => 'applications/differential/view/DifferentialRevisionDetailView.php', 'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php', 'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php', 'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php', 'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php', 'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php', 'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php', 'DifferentialRevisionIDField' => 'applications/differential/customfield/DifferentialRevisionIDField.php', 'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php', 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', 'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php', 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', 'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php', 'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php', 'DifferentialRevisionReviewersHeraldField' => 'applications/differential/herald/DifferentialRevisionReviewersHeraldField.php', 'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php', 'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php', 'DifferentialRevisionSummaryHeraldField' => 'applications/differential/herald/DifferentialRevisionSummaryHeraldField.php', 'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php', 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php', 'DifferentialSearchIndexer' => 'applications/differential/search/DifferentialSearchIndexer.php', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php', 'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php', 'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php', 'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php', 'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php', 'DifferentialTitleField' => 'applications/differential/customfield/DifferentialTitleField.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', 'DifferentialUpdateUnitResultsConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php', 'DifferentialViewPolicyField' => 'applications/differential/customfield/DifferentialViewPolicyField.php', 'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php', + 'DiffusionAuditorsAddAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php', + 'DiffusionAuditorsAddSelfHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php', + 'DiffusionAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsHeraldAction.php', + 'DiffusionBlockHeraldAction' => 'applications/diffusion/herald/DiffusionBlockHeraldAction.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', 'DiffusionBrowseDirectoryController' => 'applications/diffusion/controller/DiffusionBrowseDirectoryController.php', 'DiffusionBrowseFileController' => 'applications/diffusion/controller/DiffusionBrowseFileController.php', 'DiffusionBrowseMainController' => 'applications/diffusion/controller/DiffusionBrowseMainController.php', 'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php', 'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php', 'DiffusionBrowseSearchController' => 'applications/diffusion/controller/DiffusionBrowseSearchController.php', 'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php', 'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php', 'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php', 'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php', 'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php', 'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php', 'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php', 'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php', 'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php', 'DiffusionCommitChangeTableView' => 'applications/diffusion/view/DiffusionCommitChangeTableView.php', 'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.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', 'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php', 'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php', 'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php', 'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php', 'DiffusionCommitHeraldField' => 'applications/diffusion/herald/DiffusionCommitHeraldField.php', 'DiffusionCommitHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionCommitHeraldFieldGroup.php', 'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php', 'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.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', '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', 'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php', 'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php', 'DiffusionCommitReviewerHeraldField' => 'applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php', 'DiffusionCommitRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php', 'DiffusionCommitRevisionHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionHeraldField.php', 'DiffusionCommitRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php', 'DiffusionCommitRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionSubscribersHeraldField.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', 'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php', 'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php', 'DiffusionCreateCommentConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php', 'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.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', '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', 'DiffusionFileContent' => 'applications/diffusion/data/DiffusionFileContent.php', 'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php', 'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php', 'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php', 'DiffusionGetCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php', 'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php', 'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php', 'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php', 'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php', 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 'DiffusionGitFileContentQueryTestCase' => 'applications/diffusion/query/__tests__/DiffusionGitFileContentQueryTestCase.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', 'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php', 'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php', 'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php', 'DiffusionHovercardEventListener' => 'applications/diffusion/events/DiffusionHovercardEventListener.php', 'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php', 'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.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', 'DiffusionLintDetailsController' => 'applications/diffusion/controller/DiffusionLintDetailsController.php', 'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php', 'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php', 'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php', 'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.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', 'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.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', 'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php', 'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php', 'DiffusionMirrorDeleteController' => 'applications/diffusion/controller/DiffusionMirrorDeleteController.php', 'DiffusionMirrorEditController' => 'applications/diffusion/controller/DiffusionMirrorEditController.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', 'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php', 'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php', 'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php', 'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.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', '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', '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', 'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php', 'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php', 'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.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', 'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php', 'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php', 'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php', 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php', 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php', 'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php', 'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php', 'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php', 'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php', 'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php', 'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php', 'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php', 'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php', 'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php', 'DiffusionRepositoryEditEncodingController' => 'applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php', 'DiffusionRepositoryEditHostingController' => 'applications/diffusion/controller/DiffusionRepositoryEditHostingController.php', 'DiffusionRepositoryEditMainController' => 'applications/diffusion/controller/DiffusionRepositoryEditMainController.php', 'DiffusionRepositoryEditStagingController' => 'applications/diffusion/controller/DiffusionRepositoryEditStagingController.php', 'DiffusionRepositoryEditStorageController' => 'applications/diffusion/controller/DiffusionRepositoryEditStorageController.php', 'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php', 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', 'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php', 'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php', 'DiffusionRepositorySymbolsController' => 'applications/diffusion/controller/DiffusionRepositorySymbolsController.php', 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.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', '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', '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', 'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php', 'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php', 'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.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', 'DivinerAtomSearchIndexer' => 'applications/diviner/search/DivinerAtomSearchIndexer.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', 'DivinerBookSearchIndexer' => 'applications/diviner/search/DivinerBookSearchIndexer.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', 'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php', 'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php', 'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php', 'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.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', 'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php', 'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php', 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', 'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.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', 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', 'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php', 'DrydockBlueprintCreateController' => 'applications/drydock/controller/DrydockBlueprintCreateController.php', 'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php', 'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.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', 'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php', 'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php', 'DrydockBlueprintScopeGuard' => 'applications/drydock/util/DrydockBlueprintScopeGuard.php', 'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php', 'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php', 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.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', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', 'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php', 'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php', 'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php', 'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php', 'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', 'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php', 'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', 'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.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', 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', 'DrydockManagementCreateResourceWorkflow' => 'applications/drydock/management/DrydockManagementCreateResourceWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', 'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php', 'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php', 'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php', 'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php', 'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php', 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.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', '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', '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', 'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php', 'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php', 'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.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', 'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php', 'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php', 'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php', 'FundInitiativeIndexer' => 'applications/fund/search/FundInitiativeIndexer.php', 'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php', 'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php', 'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php', 'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php', 'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php', 'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php', 'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php', 'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php', 'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php', 'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php', 'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php', 'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.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', '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', 'HarbormasterBuildLintMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php', 'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php', 'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php', 'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.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', 'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', 'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php', 'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php', 'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php', 'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.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', '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', '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', 'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php', 'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php', 'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php', 'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php', 'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.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', + 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', 'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php', 'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php', 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php', + 'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php', 'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php', 'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', 'HarbormasterManagePlansCapability' => 'applications/harbormaster/capability/HarbormasterManagePlansCapability.php', 'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php', 'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php', 'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php', + 'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', + 'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.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', 'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.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', 'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.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', 'HarbormasterUnitMessagesController' => 'applications/harbormaster/controller/HarbormasterUnitMessagesController.php', 'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php', 'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php', 'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php', 'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.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', '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', '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', - 'HeraldCustomAction' => 'applications/herald/extension/HeraldCustomAction.php', 'HeraldDAO' => 'applications/herald/storage/HeraldDAO.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', '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', 'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.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', 'HeraldPholioMockAdapter' => 'applications/pholio/herald/HeraldPholioMockAdapter.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', 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', 'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php', 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', 'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php', 'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php', 'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php', 'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php', 'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php', 'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.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', 'HeraldSubscribersField' => 'applications/subscriptions/herald/HeraldSubscribersField.php', + 'HeraldSupportActionGroup' => 'applications/herald/action/HeraldSupportActionGroup.php', 'HeraldSupportFieldGroup' => 'applications/herald/field/HeraldSupportFieldGroup.php', 'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.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', 'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php', 'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.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', 'Javelin' => 'infrastructure/javelin/Javelin.php', 'JavelinReactorUIExample' => 'applications/uiexample/examples/JavelinReactorUIExample.php', 'JavelinUIExample' => 'applications/uiexample/examples/JavelinUIExample.php', 'JavelinViewExampleServerView' => 'applications/uiexample/examples/JavelinViewExampleServerView.php', 'JavelinViewUIExample' => 'applications/uiexample/examples/JavelinViewUIExample.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', 'LegalpadDocumentCommentController' => 'applications/legalpad/controller/LegalpadDocumentCommentController.php', 'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php', 'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php', 'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php', 'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php', 'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php', 'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php', 'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php', 'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.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', 'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php', 'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.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', 'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php', 'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php', 'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php', 'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.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', 'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php', 'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php', 'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php', 'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php', 'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.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', 'ManiphestEditAssignCapability' => 'applications/maniphest/capability/ManiphestEditAssignCapability.php', 'ManiphestEditPoliciesCapability' => 'applications/maniphest/capability/ManiphestEditPoliciesCapability.php', 'ManiphestEditPriorityCapability' => 'applications/maniphest/capability/ManiphestEditPriorityCapability.php', 'ManiphestEditProjectsCapability' => 'applications/maniphest/capability/ManiphestEditProjectsCapability.php', 'ManiphestEditStatusCapability' => 'applications/maniphest/capability/ManiphestEditStatusCapability.php', 'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php', 'ManiphestExcelDefaultFormat' => 'applications/maniphest/export/ManiphestExcelDefaultFormat.php', 'ManiphestExcelFormat' => 'applications/maniphest/export/ManiphestExcelFormat.php', 'ManiphestExcelFormatTestCase' => 'applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php', 'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php', 'ManiphestHovercardEventListener' => 'applications/maniphest/event/ManiphestHovercardEventListener.php', 'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php', 'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php', 'ManiphestNameIndexEventListener' => 'applications/maniphest/event/ManiphestNameIndexEventListener.php', 'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.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', 'ManiphestSearchIndexer' => 'applications/maniphest/search/ManiphestSearchIndexer.php', 'ManiphestStatusConfigOptionType' => 'applications/maniphest/config/ManiphestStatusConfigOptionType.php', 'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php', 'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.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', 'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php', 'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php', 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', 'ManiphestTaskDescriptionHeraldField' => 'applications/maniphest/herald/ManiphestTaskDescriptionHeraldField.php', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php', 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php', 'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php', 'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php', 'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php', 'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php', 'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php', 'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php', 'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php', 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', 'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php', 'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php', 'ManiphestTaskPriorityHeraldField' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldField.php', 'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.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', 'ManiphestTaskStatusHeraldField' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldField.php', 'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php', 'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php', 'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php', 'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php', 'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php', 'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php', 'ManiphestTransactionPreviewController' => 'applications/maniphest/controller/ManiphestTransactionPreviewController.php', 'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php', 'ManiphestTransactionSaveController' => 'applications/maniphest/controller/ManiphestTransactionSaveController.php', 'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php', 'ManiphestView' => 'applications/maniphest/view/ManiphestView.php', 'MetaMTAConstants' => 'applications/metamta/constants/MetaMTAConstants.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', 'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php', 'NuanceController' => 'applications/nuance/controller/NuanceController.php', 'NuanceCreateItemConduitAPIMethod' => 'applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php', 'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php', 'NuanceItem' => 'applications/nuance/storage/NuanceItem.php', 'NuanceItemEditController' => 'applications/nuance/controller/NuanceItemEditController.php', 'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php', 'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php', 'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php', 'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php', 'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php', 'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php', 'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php', 'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php', 'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php', 'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php', 'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php', 'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php', 'NuanceQueueItem' => 'applications/nuance/storage/NuanceQueueItem.php', 'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.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', 'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php', 'NuanceRequestor' => 'applications/nuance/storage/NuanceRequestor.php', 'NuanceRequestorEditController' => 'applications/nuance/controller/NuanceRequestorEditController.php', 'NuanceRequestorEditor' => 'applications/nuance/editor/NuanceRequestorEditor.php', 'NuanceRequestorPHIDType' => 'applications/nuance/phid/NuanceRequestorPHIDType.php', 'NuanceRequestorQuery' => 'applications/nuance/query/NuanceRequestorQuery.php', 'NuanceRequestorSource' => 'applications/nuance/storage/NuanceRequestorSource.php', 'NuanceRequestorTransaction' => 'applications/nuance/storage/NuanceRequestorTransaction.php', 'NuanceRequestorTransactionComment' => 'applications/nuance/storage/NuanceRequestorTransactionComment.php', 'NuanceRequestorTransactionQuery' => 'applications/nuance/query/NuanceRequestorTransactionQuery.php', 'NuanceRequestorViewController' => 'applications/nuance/controller/NuanceRequestorViewController.php', 'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php', 'NuanceSource' => 'applications/nuance/storage/NuanceSource.php', 'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php', 'NuanceSourceCreateController' => 'applications/nuance/controller/NuanceSourceCreateController.php', 'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.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', 'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php', 'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php', 'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.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', 'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php', 'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php', 'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php', 'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php', 'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.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', 'PHUIBadgeBoxView' => 'view/phui/PHUIBadgeBoxView.php', 'PHUIBadgeExample' => 'applications/uiexample/examples/PHUIBadgeExample.php', 'PHUIBadgeMiniView' => 'view/phui/PHUIBadgeMiniView.php', 'PHUIBadgeView' => 'view/phui/PHUIBadgeView.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', 'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php', 'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php', 'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php', 'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php', 'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php', 'PHUICrumbView' => 'view/phui/PHUICrumbView.php', 'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php', 'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php', 'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.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', 'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php', 'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php', 'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php', 'PHUIDocumentExample' => 'applications/uiexample/examples/PHUIDocumentExample.php', 'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php', 'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php', 'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php', 'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php', 'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php', 'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php', 'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php', 'PHUIFormMultiSubmitControl' => 'view/form/control/PHUIFormMultiSubmitControl.php', 'PHUIFormPageView' => 'view/form/PHUIFormPageView.php', 'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php', 'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php', 'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php', 'PHUIHeaderView' => 'view/phui/PHUIHeaderView.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', 'PHUIInfoPanelExample' => 'applications/uiexample/examples/PHUIInfoPanelExample.php', 'PHUIInfoPanelView' => 'view/phui/PHUIInfoPanelView.php', 'PHUIInfoView' => 'view/form/PHUIInfoView.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', 'PHUIPagedFormView' => 'view/form/PHUIPagedFormView.php', 'PHUIPagerView' => 'view/phui/PHUIPagerView.php', 'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php', 'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php', 'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php', 'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php', 'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php', 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', 'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', 'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php', 'PHUITagView' => 'view/phui/PHUITagView.php', 'PHUITextExample' => 'applications/uiexample/examples/PHUITextExample.php', 'PHUITextView' => 'view/phui/PHUITextView.php', 'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php', 'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php', 'PHUITimelineView' => 'view/phui/PHUITimelineView.php', 'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php', 'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php', 'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.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', 'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php', 'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php', 'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php', 'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php', 'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php', 'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.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', 'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php', 'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php', 'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php', 'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php', 'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.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', 'PassphraseSearchIndexer' => 'applications/passphrase/search/PassphraseSearchIndexer.php', 'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.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', 'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', 'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php', 'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php', 'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php', 'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php', 'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php', 'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.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', 'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php', 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', 'PhabricatorAdministratorsPolicyRule' => 'applications/policy/rule/PhabricatorAdministratorsPolicyRule.php', 'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php', 'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php', 'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php', 'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.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', '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', 'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php', 'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php', 'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php', 'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.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', 'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.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', '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', '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', 'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php', 'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php', 'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php', 'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.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', 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', 'PhabricatorAuditAddCommentController' => 'applications/audit/controller/PhabricatorAuditAddCommentController.php', 'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php', 'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php', 'PhabricatorAuditCommitStatusConstants' => 'applications/audit/constants/PhabricatorAuditCommitStatusConstants.php', 'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php', 'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php', 'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php', 'PhabricatorAuditListController' => 'applications/audit/controller/PhabricatorAuditListController.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', 'PhabricatorAuditPreviewController' => 'applications/audit/controller/PhabricatorAuditPreviewController.php', 'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php', 'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.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', 'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php', 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.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', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.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', 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', 'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php', 'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php', 'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php', 'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php', 'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php', 'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php', 'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php', 'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php', 'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php', 'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php', 'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php', 'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php', 'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php', 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', 'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.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', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php', 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', 'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php', 'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php', 'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php', 'PhabricatorAuthSSHKeyDeleteController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php', 'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php', 'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php', 'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php', 'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php', 'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.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', 'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php', 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', 'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php', 'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php', 'PhabricatorBadgeHasRecipientEdgeType' => 'applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php', 'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php', 'PhabricatorBadgesBadge' => 'applications/badges/storage/PhabricatorBadgesBadge.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', 'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php', 'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php', 'PhabricatorBadgesEditIconController' => 'applications/badges/controller/PhabricatorBadgesEditIconController.php', 'PhabricatorBadgesEditRecipientsController' => 'applications/badges/controller/PhabricatorBadgesEditRecipientsController.php', 'PhabricatorBadgesEditor' => 'applications/badges/editor/PhabricatorBadgesEditor.php', 'PhabricatorBadgesIcon' => 'applications/badges/icon/PhabricatorBadgesIcon.php', 'PhabricatorBadgesListController' => 'applications/badges/controller/PhabricatorBadgesListController.php', + 'PhabricatorBadgesMailReceiver' => 'applications/badges/mail/PhabricatorBadgesMailReceiver.php', 'PhabricatorBadgesPHIDType' => 'applications/badges/phid/PhabricatorBadgesPHIDType.php', 'PhabricatorBadgesQuery' => 'applications/badges/query/PhabricatorBadgesQuery.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', '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', 'PhabricatorBarePageUIExample' => 'applications/uiexample/examples/PhabricatorBarePageUIExample.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', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', 'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php', 'PhabricatorBotFlowdockProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php', 'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php', 'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php', 'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php', 'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php', 'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php', 'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php', 'PhabricatorBotTarget' => 'infrastructure/daemon/bot/target/PhabricatorBotTarget.php', 'PhabricatorBotUser' => 'infrastructure/daemon/bot/target/PhabricatorBotUser.php', 'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php', 'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php', 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', 'PhabricatorBusyUIExample' => 'applications/uiexample/examples/PhabricatorBusyUIExample.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.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', 'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php', 'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php', 'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php', 'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.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', 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php', 'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php', 'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php', 'PhabricatorCalendarEventEditIconController' => 'applications/calendar/controller/PhabricatorCalendarEventEditIconController.php', 'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php', 'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php', 'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php', 'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php', 'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php', 'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php', 'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php', 'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php', 'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php', 'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php', 'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php', 'PhabricatorCalendarEventSearchIndexer' => 'applications/calendar/search/PhabricatorCalendarEventSearchIndexer.php', 'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php', 'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php', 'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php', 'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarIcon' => 'applications/calendar/icon/PhabricatorCalendarIcon.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', 'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php', 'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php', 'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php', 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.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', 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.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', 'PhabricatorConduitCertificateSettingsPanel' => 'applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/PhabricatorConduitConnectionLog.php', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php', 'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php', 'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php', 'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php', 'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php', 'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php', 'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php', 'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.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', 'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php', 'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php', 'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.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', 'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php', 'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php', 'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php', 'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.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', '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', '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', 'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php', 'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php', 'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php', 'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.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', 'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php', 'PhabricatorConfigWelcomeController' => 'applications/config/controller/PhabricatorConfigWelcomeController.php', 'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php', 'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php', 'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php', 'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php', 'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php', 'PhabricatorContentSourceView' => 'applications/metamta/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', 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php', 'PhabricatorCountdownCommentController' => 'applications/countdown/controller/PhabricatorCountdownCommentController.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', 'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/PhabricatorCountdownDeleteController.php', 'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php', 'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.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', 'PhabricatorCountdownTransaction' => 'applications/countdown/storage/PhabricatorCountdownTransaction.php', 'PhabricatorCountdownTransactionComment' => 'applications/countdown/storage/PhabricatorCountdownTransactionComment.php', 'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php', 'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php', 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCredentialsUsedByObjectEdgeType' => 'applications/passphrase/edge/PhabricatorCredentialsUsedByObjectEdgeType.php', 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', 'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php', 'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php', 'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php', 'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.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', 'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php', 'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php', 'PhabricatorCustomHeaderConfigType' => 'applications/config/custom/PhabricatorCustomHeaderConfigType.php', 'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.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', 'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php', 'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php', 'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.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', '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', 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', 'PhabricatorDashboardCopyController' => 'applications/dashboard/controller/PhabricatorDashboardCopyController.php', 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', 'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php', 'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php', 'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php', 'PhabricatorDashboardHistoryController' => 'applications/dashboard/controller/PhabricatorDashboardHistoryController.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', 'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php', 'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php', 'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php', 'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php', 'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php', 'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php', 'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.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', 'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.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', 'PhabricatorDashboardUninstallController' => 'applications/dashboard/controller/PhabricatorDashboardUninstallController.php', 'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php', 'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php', 'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php', 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', 'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php', 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 'PhabricatorDesktopNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php', 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', 'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.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', 'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php', 'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php', 'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php', 'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.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', 'PhabricatorDisqusConfigOptions' => 'applications/config/option/PhabricatorDisqusConfigOptions.php', 'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php', 'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', 'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php', 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php', 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php', 'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php', 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.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', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorElasticSearchEngine' => 'applications/search/engine/PhabricatorElasticSearchEngine.php', 'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php', 'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php', 'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php', 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', 'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php', 'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php', 'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php', 'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php', 'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php', 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.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', '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', 'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php', 'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php', 'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php', 'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php', 'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.php', 'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php', 'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php', 'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php', 'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php', 'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php', 'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php', 'PhabricatorFactLastUpdatedEngine' => 'applications/fact/engine/PhabricatorFactLastUpdatedEngine.php', 'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php', 'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php', 'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php', 'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php', 'PhabricatorFactManagementStatusWorkflow' => 'applications/fact/management/PhabricatorFactManagementStatusWorkflow.php', 'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', 'PhabricatorFactSimpleSpec' => 'applications/fact/spec/PhabricatorFactSimpleSpec.php', 'PhabricatorFactSpec' => 'applications/fact/spec/PhabricatorFactSpec.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.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', 'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.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', 'PhabricatorFileCommentController' => 'applications/files/controller/PhabricatorFileCommentController.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', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', 'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php', 'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php', 'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php', 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 'PhabricatorFileLinkListView' => 'view/layout/PhabricatorFileLinkListView.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', 'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php', 'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.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', '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', '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', 'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php', 'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php', 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', 'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php', 'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php', 'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php', 'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php', 'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php', 'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php', 'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php', 'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php', 'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.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', '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', 'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php', 'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php', 'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php', 'PhabricatorGarbageCollectorConfigOptions' => 'applications/config/option/PhabricatorGarbageCollectorConfigOptions.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', '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', 'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php', 'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.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', 'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php', 'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php', 'PhabricatorHomeMainController' => 'applications/home/controller/PhabricatorHomeMainController.php', 'PhabricatorHomePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorHomePreferencesSettingsPanel.php', 'PhabricatorHomeQuickCreateController' => 'applications/home/controller/PhabricatorHomeQuickCreateController.php', 'PhabricatorHovercardUIExample' => 'applications/uiexample/examples/PhabricatorHovercardUIExample.php', 'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php', 'PhabricatorHunksManagementMigrateWorkflow' => 'applications/differential/management/PhabricatorHunksManagementMigrateWorkflow.php', 'PhabricatorHunksManagementWorkflow' => 'applications/differential/management/PhabricatorHunksManagementWorkflow.php', 'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php', 'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php', 'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php', 'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', 'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.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', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', 'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php', 'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', 'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php', 'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php', 'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php', 'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php', 'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php', 'PhabricatorLegalpadSignaturePolicyRule' => 'applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php', 'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php', 'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.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', 'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php', 'PhabricatorListFilterUIExample' => 'applications/uiexample/examples/PhabricatorListFilterUIExample.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', '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', 'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php', 'PhabricatorMacroCommentController' => 'applications/macro/controller/PhabricatorMacroCommentController.php', 'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php', 'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php', 'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php', 'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php', 'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php', 'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.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', 'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php', 'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php', 'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php', 'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php', 'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php', 'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php', 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.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', + 'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php', 'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php', 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php', 'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php', 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', 'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php', 'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php', 'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php', 'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php', 'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php', 'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php', 'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php', 'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', 'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php', 'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.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', 'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.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', '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', 'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php', 'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php', 'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.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', 'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.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', 'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 'PhabricatorMySQLSearchEngine' => 'applications/search/engine/PhabricatorMySQLSearchEngine.php', 'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php', 'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php', 'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php', 'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php', 'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.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', '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', 'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php', 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', 'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php', 'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php', 'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php', 'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.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', 'PhabricatorOAuthClientDeleteController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php', 'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php', 'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php', 'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.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', 'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php', 'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php', 'PhabricatorOAuthServerTestController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTestController.php', 'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php', 'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php', 'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php', 'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php', 'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.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', 'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php', 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 'PhabricatorObjectUsesCredentialsEdgeType' => 'applications/transactions/edges/PhabricatorObjectUsesCredentialsEdgeType.php', 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php', 'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php', 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', 'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php', 'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php', 'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php', 'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php', 'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php', 'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php', 'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php', 'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php', 'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php', 'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php', 'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php', 'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php', 'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php', 'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php', 'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php', 'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php', 'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php', 'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php', 'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php', 'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php', 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', 'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php', 'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php', 'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php', 'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php', 'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php', 'PhabricatorPHPMailerConfigOptions' => 'applications/config/option/PhabricatorPHPMailerConfigOptions.php', 'PhabricatorPagedFormUIExample' => 'applications/uiexample/examples/PhabricatorPagedFormUIExample.php', 'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php', 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.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', 'PhabricatorPasteCommentController' => 'applications/paste/controller/PhabricatorPasteCommentController.php', 'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php', 'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php', 'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php', 'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php', 'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php', 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php', 'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php', 'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php', 'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php', 'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php', 'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php', 'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php', 'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php', 'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.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', 'PhabricatorPeopleCalendarController' => 'applications/people/controller/PhabricatorPeopleCalendarController.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', 'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php', 'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php', 'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php', 'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php', 'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php', 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', 'PhabricatorPeopleHovercardEventListener' => 'applications/people/event/PhabricatorPeopleHovercardEventListener.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', 'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php', 'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php', 'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php', 'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php', 'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php', 'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php', 'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php', 'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php', 'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.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', 'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php', 'PhabricatorPersonaAuthProvider' => 'applications/auth/provider/PhabricatorPersonaAuthProvider.php', 'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php', 'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php', 'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php', 'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php', 'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php', 'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php', 'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php', 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', 'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php', 'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php', 'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php', 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', 'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php', 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', 'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php', 'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php', 'PhabricatorPhurlDAO' => 'applications/phurl/storage/PhabricatorPhurlDAO.php', 'PhabricatorPhurlSchemaSpec' => 'applications/phurl/storage/PhabricatorPhurlSchemaSpec.php', 'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php', 'PhabricatorPhurlURLEditController' => 'applications/phurl/controller/PhabricatorPhurlURLEditController.php', 'PhabricatorPhurlURLEditor' => 'applications/phurl/editor/PhabricatorPhurlURLEditor.php', 'PhabricatorPhurlURLListController' => 'applications/phurl/controller/PhabricatorPhurlURLListController.php', 'PhabricatorPhurlURLPHIDType' => 'applications/phurl/phid/PhabricatorPhurlURLPHIDType.php', 'PhabricatorPhurlURLQuery' => 'applications/phurl/query/PhabricatorPhurlURLQuery.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', 'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php', 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.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', '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', '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', 'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php', 'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.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', 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.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', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', + 'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', 'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php', 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.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', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectEditDetailsController' => 'applications/project/controller/PhabricatorProjectEditDetailsController.php', 'PhabricatorProjectEditIconController' => 'applications/project/controller/PhabricatorProjectEditIconController.php', 'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php', 'PhabricatorProjectEditorTestCase' => 'applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php', 'PhabricatorProjectFeedController' => 'applications/project/controller/PhabricatorProjectFeedController.php', + 'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php', 'PhabricatorProjectIcon' => 'applications/project/icon/PhabricatorProjectIcon.php', 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php', 'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php', 'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php', 'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php', 'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php', 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php', 'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php', 'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.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', 'PhabricatorProjectSearchIndexer' => 'applications/project/search/PhabricatorProjectSearchIndexer.php', 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php', 'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php', 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.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', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php', 'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php', 'PhabricatorProjectsPolicyRule' => 'applications/policy/rule/PhabricatorProjectsPolicyRule.php', 'PhabricatorProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php', 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', 'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php', 'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php', 'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php', 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', 'PhabricatorRecipientHasBadgeEdgeType' => 'applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', 'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php', 'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php', 'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php', 'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.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', 'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php', 'PhabricatorRemarkupGraphvizBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupGraphvizBlockInterpreter.php', 'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php', 'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php', 'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php', 'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php', 'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.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', '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', 'PhabricatorRepositoryCommitSearchIndexer' => 'applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php', 'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php', 'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php', 'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php', 'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php', 'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php', 'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php', 'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php', 'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', 'PhabricatorRepositoryManagementEditWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.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', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php', 'PhabricatorRepositoryManagementMovePathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMovePathsWorkflow.php', 'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php', 'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php', 'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php', 'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.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', 'PhabricatorRepositoryMirrorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryMirrorPHIDType.php', 'PhabricatorRepositoryMirrorQuery' => 'applications/repository/query/PhabricatorRepositoryMirrorQuery.php', 'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php', 'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php', 'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.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', 'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php', 'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php', 'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php', 'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php', 'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php', 'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php', 'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php', 'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.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', 'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php', 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', 'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php', 'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php', 'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php', 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php', 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', 'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php', 'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php', 'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php', 'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php', 'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php', 'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php', 'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php', 'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php', 'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php', 'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php', 'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php', 'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php', 'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.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', 'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php', 'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php', 'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php', 'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.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', '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', 'PhabricatorSearchDocumentIndexer' => 'applications/search/index/PhabricatorSearchDocumentIndexer.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', 'PhabricatorSearchEngine' => 'applications/search/engine/PhabricatorSearchEngine.php', 'PhabricatorSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php', 'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php', 'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php', 'PhabricatorSearchIndexer' => 'applications/search/index/PhabricatorSearchIndexer.php', 'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php', 'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php', 'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php', 'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php', 'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php', 'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php', 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', 'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.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', 'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php', 'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php', 'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php', 'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php', 'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php', 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php', 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', 'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php', 'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php', 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', 'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php', 'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php', 'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.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', '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', + 'PhabricatorSlowvoteReplyHandler' => 'applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php', 'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php', 'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php', 'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php', 'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php', 'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php', 'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php', 'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php', 'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php', 'PhabricatorSortTableUIExample' => 'applications/uiexample/examples/PhabricatorSortTableUIExample.php', 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.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', 'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php', 'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php', 'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php', 'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php', 'PhabricatorSpacesNamespaceEditor' => 'applications/spaces/editor/PhabricatorSpacesNamespaceEditor.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', 'PhabricatorSpacesNoAccessController' => 'applications/spaces/controller/PhabricatorSpacesNoAccessController.php', 'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php', 'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.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', 'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php', 'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.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', 'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.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', 'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php', 'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php', 'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.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', 'PhabricatorStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.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', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', + 'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php', 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', + 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php', + 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.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', 'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php', 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.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', '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', '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', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php', 'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php', 'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.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', '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', 'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php', 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php', 'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php', 'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php', 'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.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', 'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php', 'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php', - 'PhabricatorTwoColumnUIExample' => 'applications/uiexample/examples/PhabricatorTwoColumnUIExample.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', 'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php', 'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php', 'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php', 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.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', 'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php', 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.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', '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', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php', 'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php', 'PhabricatorUserProfileEditor' => 'applications/people/editor/PhabricatorUserProfileEditor.php', 'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php', 'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php', 'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php', 'PhabricatorUserSearchIndexer' => 'applications/people/search/PhabricatorUserSearchIndexer.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', 'PhabricatorUsersPolicyRule' => 'applications/policy/rule/PhabricatorUsersPolicyRule.php', 'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php', 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.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', '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', 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', 'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.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', 'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php', 'PhabricatorXHPASTViewDAO' => 'applications/phpast/storage/PhabricatorXHPASTViewDAO.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', 'PhabricatorXHPASTViewParseTree' => 'applications/phpast/storage/PhabricatorXHPASTViewParseTree.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', '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', 'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php', 'PhameBasicBlogSkin' => 'applications/phame/skins/PhameBasicBlogSkin.php', 'PhameBasicTemplateBlogSkin' => 'applications/phame/skins/PhameBasicTemplateBlogSkin.php', 'PhameBlog' => 'applications/phame/storage/PhameBlog.php', 'PhameBlogDeleteController' => 'applications/phame/controller/blog/PhameBlogDeleteController.php', 'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php', 'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php', 'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php', 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', 'PhameBlogResourceSite' => 'applications/phame/site/PhameBlogResourceSite.php', 'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php', 'PhameBlogSite' => 'applications/phame/site/PhameBlogSite.php', 'PhameBlogSkin' => 'applications/phame/skins/PhameBlogSkin.php', 'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php', 'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php', 'PhameCelerityResources' => 'applications/phame/celerity/PhameCelerityResources.php', 'PhameConduitAPIMethod' => 'applications/phame/conduit/PhameConduitAPIMethod.php', 'PhameController' => 'applications/phame/controller/PhameController.php', 'PhameCreatePostConduitAPIMethod' => 'applications/phame/conduit/PhameCreatePostConduitAPIMethod.php', 'PhameDAO' => 'applications/phame/storage/PhameDAO.php', 'PhamePost' => 'applications/phame/storage/PhamePost.php', 'PhamePostDeleteController' => 'applications/phame/controller/post/PhamePostDeleteController.php', 'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php', 'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php', 'PhamePostFramedController' => 'applications/phame/controller/post/PhamePostFramedController.php', 'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php', 'PhamePostNewController' => 'applications/phame/controller/post/PhamePostNewController.php', 'PhamePostNotLiveController' => 'applications/phame/controller/post/PhamePostNotLiveController.php', 'PhamePostPreviewController' => 'applications/phame/controller/post/PhamePostPreviewController.php', 'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php', 'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php', 'PhamePostSearchEngine' => 'applications/phame/query/PhamePostSearchEngine.php', 'PhamePostTransaction' => 'applications/phame/storage/PhamePostTransaction.php', 'PhamePostTransactionQuery' => 'applications/phame/query/PhamePostTransactionQuery.php', 'PhamePostUnpublishController' => 'applications/phame/controller/post/PhamePostUnpublishController.php', 'PhamePostView' => 'applications/phame/view/PhamePostView.php', 'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php', 'PhameQueryConduitAPIMethod' => 'applications/phame/conduit/PhameQueryConduitAPIMethod.php', 'PhameQueryPostsConduitAPIMethod' => 'applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php', 'PhameResourceController' => 'applications/phame/controller/PhameResourceController.php', 'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php', 'PhameSite' => 'applications/phame/site/PhameSite.php', 'PhameSkinSpecification' => 'applications/phame/skins/PhameSkinSpecification.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', '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', 'PholioActionMenuEventListener' => 'applications/pholio/event/PholioActionMenuEventListener.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', 'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php', 'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.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', 'PholioMockAuthorHeraldField' => 'applications/pholio/herald/PholioMockAuthorHeraldField.php', 'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php', 'PholioMockDescriptionHeraldField' => 'applications/pholio/herald/PholioMockDescriptionHeraldField.php', 'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php', 'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php', 'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php', 'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php', 'PholioMockHeraldField' => 'applications/pholio/herald/PholioMockHeraldField.php', 'PholioMockHeraldFieldGroup' => 'applications/pholio/herald/PholioMockHeraldFieldGroup.php', 'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php', 'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php', 'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php', 'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php', 'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php', 'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php', 'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php', 'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.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', 'PholioSearchIndexer' => 'applications/pholio/search/PholioSearchIndexer.php', 'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php', 'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php', 'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php', 'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php', 'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php', 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 'PhortuneAccountEditController' => 'applications/phortune/controller/PhortuneAccountEditController.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', 'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php', 'PhortuneAccountListController' => 'applications/phortune/controller/PhortuneAccountListController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', 'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php', 'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php', 'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php', 'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php', 'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php', 'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php', 'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php', 'PhortuneCartListController' => 'applications/phortune/controller/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/PhortuneCartUpdateController.php', 'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneChargeListController' => 'applications/phortune/controller/PhortuneChargeListController.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', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.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', 'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php', 'PhortuneMerchantController' => 'applications/phortune/controller/PhortuneMerchantController.php', 'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php', 'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php', 'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php', 'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php', 'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php', 'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.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', 'PhortuneMerchantViewController' => 'applications/phortune/controller/PhortuneMerchantViewController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneNotImplementedException' => 'applications/phortune/exception/PhortuneNotImplementedException.php', 'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php', 'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php', 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/PhortunePaymentMethodCreateController.php', 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/PhortunePaymentMethodDisableController.php', 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/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/PhortuneProductListController.php', 'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php', 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php', 'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php', 'PhortuneProviderActionController' => 'applications/phortune/controller/PhortuneProviderActionController.php', 'PhortuneProviderDisableController' => 'applications/phortune/controller/PhortuneProviderDisableController.php', 'PhortuneProviderEditController' => 'applications/phortune/controller/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/PhortuneSubscriptionEditController.php', 'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php', 'PhortuneSubscriptionListController' => 'applications/phortune/controller/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/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', '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', 'PhrictionController' => 'applications/phriction/controller/PhrictionController.php', 'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php', 'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.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', 'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php', 'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php', 'PhrictionDocumentHeraldField' => 'applications/phriction/herald/PhrictionDocumentHeraldField.php', 'PhrictionDocumentHeraldFieldGroup' => 'applications/phriction/herald/PhrictionDocumentHeraldFieldGroup.php', 'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php', 'PhrictionDocumentPathHeraldField' => 'applications/phriction/herald/PhrictionDocumentPathHeraldField.php', 'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php', 'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php', 'PhrictionDocumentTitleHeraldField' => 'applications/phriction/herald/PhrictionDocumentTitleHeraldField.php', 'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php', 'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.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', 'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php', 'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php', 'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php', 'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php', 'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php', 'PhrictionSearchEngine' => 'applications/phriction/query/PhrictionSearchEngine.php', 'PhrictionSearchIndexer' => 'applications/phriction/search/PhrictionSearchIndexer.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', 'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php', 'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php', 'PonderAnswerHasVotingUserEdgeType' => 'applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php', 'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php', 'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php', 'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php', 'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php', 'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php', 'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php', 'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php', 'PonderConstants' => 'applications/ponder/constants/PonderConstants.php', 'PonderController' => 'applications/ponder/controller/PonderController.php', 'PonderDAO' => 'applications/ponder/storage/PonderDAO.php', 'PonderEditor' => 'applications/ponder/editor/PonderEditor.php', 'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php', 'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php', + 'PonderQuestionDefaultEditCapability' => 'applications/ponder/capability/PonderQuestionDefaultEditCapability.php', + 'PonderQuestionDefaultViewCapability' => 'applications/ponder/capability/PonderQuestionDefaultViewCapability.php', 'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php', 'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php', 'PonderQuestionHasVotingUserEdgeType' => 'applications/ponder/edge/PonderQuestionHasVotingUserEdgeType.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', 'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php', 'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php', 'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php', 'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php', 'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php', 'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php', 'PonderSearchIndexer' => 'applications/ponder/search/PonderSearchIndexer.php', 'PonderTransactionFeedStory' => 'applications/ponder/feed/PonderTransactionFeedStory.php', 'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php', 'PonderVotableView' => 'applications/ponder/view/PonderVotableView.php', 'PonderVote' => 'applications/ponder/constants/PonderVote.php', 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', 'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php', 'PonderVotingUserHasAnswerEdgeType' => 'applications/ponder/edge/PonderVotingUserHasAnswerEdgeType.php', 'PonderVotingUserHasQuestionEdgeType' => 'applications/ponder/edge/PonderVotingUserHasQuestionEdgeType.php', 'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php', 'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php', 'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php', 'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php', 'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php', 'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php', 'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php', 'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php', 'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.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', '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', 'RepositoryCreateConduitAPIMethod' => 'applications/repository/conduit/RepositoryCreateConduitAPIMethod.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', 'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php', 'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php', 'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php', 'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php', 'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.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_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', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', ), 'AlmanacBindingEditController' => 'AlmanacServiceController', 'AlmanacBindingEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', 'AlmanacBindingQuery' => 'AlmanacQuery', 'AlmanacBindingTableView' => 'AphrontView', 'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingViewController' => 'AlmanacServiceController', 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterServiceType' => 'AlmanacServiceType', 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', 'AlmanacCoreCustomField' => array( 'AlmanacCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCustomField' => 'PhabricatorCustomField', 'AlmanacCustomServiceType' => 'AlmanacServiceType', 'AlmanacDAO' => 'PhabricatorLiskDAO', 'AlmanacDevice' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorSSHPublicKeyInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', 'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacDeviceListController' => 'AlmanacDeviceController', 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', 'AlmanacDeviceQuery' => 'AlmanacQuery', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacDeviceViewController' => 'AlmanacDeviceController', 'AlmanacInterface' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacKeys' => 'Phobject', 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', 'AlmanacNetwork' => array( 'AlmanacDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'AlmanacNetworkController' => 'AlmanacController', 'AlmanacNetworkEditController' => 'AlmanacNetworkController', 'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacNetworkListController' => 'AlmanacNetworkController', 'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType', 'AlmanacNetworkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacNetworkViewController' => 'AlmanacNetworkController', 'AlmanacProperty' => array( 'PhabricatorCustomFieldStorage', 'PhabricatorPolicyInterface', ), 'AlmanacPropertyController' => 'AlmanacController', 'AlmanacPropertyDeleteController' => 'AlmanacDeviceController', 'AlmanacPropertyEditController' => 'AlmanacDeviceController', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'AlmanacService' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceEditController' => 'AlmanacServiceController', 'AlmanacServiceEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacServiceListController' => 'AlmanacServiceController', 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', 'AlmanacServiceQuery' => 'AlmanacQuery', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacServiceTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacServiceType' => 'Phobject', 'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase', 'AlmanacServiceViewController' => 'AlmanacServiceController', 'AphlictDropdownDataQuery' => 'Phobject', 'Aphront304Response' => 'AphrontResponse', 'Aphront400Response' => 'AphrontResponse', 'Aphront403Response' => 'AphrontHTMLResponse', 'Aphront404Response' => 'AphrontHTMLResponse', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontApplicationConfiguration' => 'Phobject', 'AphrontBarView' => 'AphrontView', 'AphrontCSRFException' => 'AphrontException', 'AphrontCalendarEventView' => 'AphrontView', 'AphrontController' => 'Phobject', 'AphrontCursorPagerView' => 'AphrontView', 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => 'AphrontView', 'AphrontException' => 'Exception', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormChooseButtonControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', 'AphrontFormDateControl' => 'AphrontFormControl', 'AphrontFormDateControlValue' => 'Phobject', 'AphrontFormDividerControl' => 'AphrontFormControl', 'AphrontFormFileControl' => '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', 'AphrontHTTPProxyResponse' => 'AphrontResponse', 'AphrontHTTPSink' => 'Phobject', 'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase', 'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase', 'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink', 'AphrontJSONResponse' => 'AphrontResponse', 'AphrontJavelinView' => 'AphrontView', 'AphrontKeyboardShortcutsAvailableView' => 'AphrontView', 'AphrontListFilterView' => 'AphrontView', 'AphrontMoreView' => 'AphrontView', 'AphrontMultiColumnView' => 'AphrontView', 'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase', 'AphrontNullView' => 'AphrontView', 'AphrontPHPHTTPSink' => 'AphrontHTTPSink', 'AphrontPageView' => 'AphrontView', 'AphrontPlainTextResponse' => 'AphrontResponse', 'AphrontProgressBarView' => 'AphrontBarView', 'AphrontProxyResponse' => 'AphrontResponse', 'AphrontRedirectResponse' => 'AphrontResponse', 'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase', 'AphrontReloadResponse' => 'AphrontRedirectResponse', 'AphrontRequest' => 'Phobject', 'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontResponse' => 'Phobject', 'AphrontSideNavFilterView' => 'AphrontView', 'AphrontSite' => 'Phobject', 'AphrontStackTraceView' => 'AphrontView', 'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse', 'AphrontTableView' => 'AphrontView', 'AphrontTagView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView', - 'AphrontTwoColumnView' => 'AphrontView', 'AphrontTypeaheadTemplateView' => 'AphrontView', 'AphrontURIMapper' => 'Phobject', 'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse', 'AphrontUsageException' => 'AphrontException', 'AphrontView' => array( 'Phobject', 'PhutilSafeHTMLProducerInterface', ), 'AphrontWebpageResponse' => 'AphrontHTMLResponse', 'ArcanistConduitAPIMethod' => 'ConduitAPIMethod', 'AuditConduitAPIMethod' => 'ConduitAPIMethod', 'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod', 'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability', 'CalendarTimeUtil' => 'Phobject', 'CalendarTimeUtilTestCase' => 'PhabricatorTestCase', 'CelerityAPI' => 'Phobject', 'CelerityDefaultPostprocessor' => 'CelerityPostprocessor', 'CelerityHighContrastPostprocessor' => 'CelerityPostprocessor', 'CelerityLargeFontPostprocessor' => 'CelerityPostprocessor', 'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow', 'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow', 'CelerityPhabricatorResourceController' => 'CelerityResourceController', 'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk', 'CelerityPhysicalResources' => 'CelerityResources', 'CelerityPhysicalResourcesTestCase' => 'PhabricatorTestCase', 'CelerityPostprocessor' => 'Phobject', 'CelerityPostprocessorTestCase' => 'PhabricatorTestCase', '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', 'ConduitCall' => 'Phobject', 'ConduitCallTestCase' => 'PhabricatorTestCase', 'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitConnectionGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitDeprecatedCallSetupCheck' => 'PhabricatorSetupCheck', 'ConduitException' => 'Exception', 'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException', 'ConduitMethodNotFoundException' => 'ConduitException', 'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow', 'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector', 'ConpherenceColumnViewController' => 'ConpherenceController', 'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod', 'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions', 'ConpherenceConstants' => 'Phobject', 'ConpherenceController' => 'PhabricatorController', 'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceCreateThreadMailReceiver' => 'PhabricatorMailReceiver', 'ConpherenceDAO' => 'PhabricatorLiskDAO', 'ConpherenceDurableColumnView' => 'AphrontTagView', 'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', 'ConpherenceFileWidgetView' => 'ConpherenceWidgetView', 'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl', 'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceHovercardEventListener' => 'PhabricatorEventListener', 'ConpherenceImageData' => 'ConpherenceConstants', 'ConpherenceIndex' => 'ConpherenceDAO', 'ConpherenceLayoutView' => 'AphrontView', 'ConpherenceListController' => 'ConpherenceController', 'ConpherenceMenuItemView' => 'AphrontTagView', 'ConpherenceNewRoomController' => 'ConpherenceController', 'ConpherenceNotificationPanelController' => 'ConpherenceController', 'ConpherenceParticipant' => 'ConpherenceDAO', 'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipationStatus' => 'ConpherenceConstants', 'ConpherencePeopleWidgetView' => 'ConpherenceWidgetView', 'ConpherencePicCropControl' => 'AphrontFormControl', 'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler', 'ConpherenceRoomListController' => 'ConpherenceController', 'ConpherenceRoomTestCase' => 'ConpherenceTestCase', 'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ConpherenceSettings' => 'ConpherenceConstants', 'ConpherenceTestCase' => 'PhabricatorTestCase', 'ConpherenceThread' => array( 'ConpherenceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMentionableInterface', 'PhabricatorDestructibleInterface', ), 'ConpherenceThreadIndexer' => 'PhabricatorSearchDocumentIndexer', 'ConpherenceThreadListView' => 'AphrontView', 'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver', 'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule', 'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ConpherenceTransaction' => 'PhabricatorApplicationTransaction', 'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ConpherenceTransactionRenderer' => 'Phobject', 'ConpherenceTransactionView' => 'AphrontView', 'ConpherenceUpdateActions' => 'ConpherenceConstants', 'ConpherenceUpdateController' => 'ConpherenceController', 'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceViewController' => 'ConpherenceController', 'ConpherenceWidgetConfigConstants' => 'ConpherenceConstants', 'ConpherenceWidgetController' => 'ConpherenceController', 'ConpherenceWidgetView' => 'AphrontView', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleCore' => 'Phobject', 'DarkConsoleDataController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleErrorLogPluginAPI' => 'Phobject', 'DarkConsoleEventPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener', 'DarkConsolePlugin' => 'Phobject', 'DarkConsoleRequestPlugin' => 'DarkConsolePlugin', 'DarkConsoleServicesPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPluginAPI' => 'Phobject', 'DefaultDatabaseConfigurationProvider' => array( 'Phobject', 'DatabaseConfigurationProvider', ), 'DifferentialAction' => 'Phobject', 'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand', 'DifferentialActionMenuEventListener' => 'PhabricatorEventListener', 'DifferentialAddCommentView' => 'AphrontView', 'DifferentialAdjustmentMapTestCase' => 'PhutilTestCase', 'DifferentialAffectedPath' => 'DifferentialDAO', 'DifferentialApplyPatchField' => 'DifferentialCustomField', 'DifferentialAsanaRepresentationField' => 'DifferentialCustomField', 'DifferentialAuditorsField' => 'DifferentialStoredCustomField', 'DifferentialAuthorField' => 'DifferentialCustomField', 'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField', + 'DifferentialBlockHeraldAction' => 'HeraldAction', 'DifferentialBranchField' => 'DifferentialCustomField', 'DifferentialChangeHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialChangeType' => 'Phobject', 'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField', 'DifferentialChangeset' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', ), 'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject', 'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetListView' => 'AphrontView', 'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer', 'DifferentialChangesetParser' => 'Phobject', 'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase', 'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialChangesetRenderer' => 'Phobject', 'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer', 'DifferentialChangesetViewController' => 'DifferentialController', 'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCommentPreviewController' => 'DifferentialController', 'DifferentialCommentSaveController' => 'DifferentialController', 'DifferentialCommitMessageParser' => 'Phobject', 'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase', 'DifferentialCommitsField' => 'DifferentialCustomField', 'DifferentialConduitAPIMethod' => 'ConduitAPIMethod', 'DifferentialConflictsField' => 'DifferentialCustomField', 'DifferentialController' => 'PhabricatorController', 'DifferentialCoreCustomField' => 'DifferentialCustomField', 'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateMailReceiver' => 'PhabricatorMailReceiver', 'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCustomField' => 'PhabricatorCustomField', 'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser', 'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase', 'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser', 'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase', 'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DifferentialDependenciesField' => 'DifferentialCustomField', 'DifferentialDependsOnField' => 'DifferentialCustomField', 'DifferentialDiff' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', 'HarbormasterBuildableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'DifferentialDiffAffectedFilesHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffAuthorHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffAuthorProjectsHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffContentAddedHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffContentHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffContentRemovedHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffCreateController' => 'DifferentialController', 'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor', 'DifferentialDiffHeraldField' => 'HeraldField', 'DifferentialDiffHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery', 'DifferentialDiffPHIDType' => 'PhabricatorPHIDType', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialDiffRepositoryHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffRepositoryProjectsHeraldField' => 'DifferentialDiffHeraldField', 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffTestCase' => 'PhutilTestCase', 'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction', 'DifferentialDiffTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher', 'DifferentialDraft' => 'DifferentialDAO', 'DifferentialEditPolicyField' => 'DifferentialCoreCustomField', 'DifferentialFieldParseException' => 'Exception', 'DifferentialFieldValidationException' => 'Exception', 'DifferentialFindConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialFinishPostponedLintersConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialGetWorkingCopy' => 'Phobject', 'DifferentialGitHubLandingStrategy' => 'DifferentialLandingStrategy', 'DifferentialGitSVNIDField' => 'DifferentialCustomField', 'DifferentialHarbormasterField' => 'DifferentialCustomField', 'DifferentialHiddenComment' => 'DifferentialDAO', 'DifferentialHostField' => 'DifferentialCustomField', 'DifferentialHostedGitLandingStrategy' => 'DifferentialLandingStrategy', 'DifferentialHostedMercurialLandingStrategy' => 'DifferentialLandingStrategy', 'DifferentialHovercardEventListener' => 'PhabricatorEventListener', 'DifferentialHunk' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', ), 'DifferentialHunkParser' => 'Phobject', 'DifferentialHunkParserTestCase' => 'PhabricatorTestCase', 'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialHunkTestCase' => 'PhutilTestCase', 'DifferentialInlineComment' => array( 'Phobject', 'PhabricatorInlineCommentInterface', ), 'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController', 'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField', 'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener', 'DifferentialLandingStrategy' => 'Phobject', 'DifferentialLegacyHunk' => 'DifferentialHunk', 'DifferentialLineAdjustmentMap' => 'Phobject', 'DifferentialLintField' => 'DifferentialHarbormasterField', 'DifferentialLintStatus' => 'Phobject', 'DifferentialLocalCommitsView' => 'AphrontView', 'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField', 'DifferentialModernHunk' => 'DifferentialHunk', 'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector', 'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', 'DifferentialPathField' => 'DifferentialCustomField', 'DifferentialPrimaryPaneView' => 'AphrontView', 'DifferentialProjectReviewersField' => 'DifferentialCustomField', 'DifferentialProjectsField' => 'DifferentialCoreCustomField', 'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialRawDiffRenderer' => 'Phobject', 'DifferentialReleephRequestFieldSpecification' => 'Phobject', 'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'DifferentialRepositoryField' => 'DifferentialCoreCustomField', 'DifferentialRepositoryLookup' => 'Phobject', 'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField', 'DifferentialRevertPlanField' => 'DifferentialStoredCustomField', 'DifferentialReviewedByField' => 'DifferentialCoreCustomField', 'DifferentialReviewer' => 'Phobject', 'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialReviewerStatus' => 'Phobject', + 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction', + 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction', + 'DifferentialReviewersAddReviewersHeraldAction' => 'DifferentialReviewersHeraldAction', + 'DifferentialReviewersAddSelfHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersField' => 'DifferentialCoreCustomField', + 'DifferentialReviewersHeraldAction' => 'HeraldAction', 'DifferentialReviewersView' => 'AphrontView', 'DifferentialRevision' => array( 'DifferentialDAO', 'PhabricatorTokenReceiverInterface', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorFlaggableInterface', 'PhrequentTrackableInterface', 'HarbormasterBuildableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMentionableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', ), 'DifferentialRevisionAffectedFilesHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionAuthorHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionAuthorProjectsHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionCloseDetailsController' => 'DifferentialController', 'DifferentialRevisionContentAddedHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionContentHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionContentRemovedHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionControlSystem' => 'Phobject', 'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionDetailView' => 'AphrontView', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHeraldField' => 'HeraldField', 'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialRevisionIDField' => 'DifferentialCustomField', 'DifferentialRevisionLandController' => 'DifferentialController', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionListView' => 'AphrontView', 'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver', 'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType', 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionReviewersHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DifferentialRevisionStatus' => 'Phobject', 'DifferentialRevisionSummaryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialStoredCustomField' => 'DifferentialCustomField', 'DifferentialSubscribersField' => 'DifferentialCoreCustomField', 'DifferentialSummaryField' => 'DifferentialCoreCustomField', 'DifferentialTestPlanField' => 'DifferentialCoreCustomField', 'DifferentialTitleField' => 'DifferentialCoreCustomField', 'DifferentialTransaction' => 'PhabricatorApplicationTransaction', 'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment', 'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView', 'DifferentialUnitField' => 'DifferentialHarbormasterField', 'DifferentialUnitStatus' => 'Phobject', 'DifferentialUnitTestResult' => 'Phobject', 'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialUpdateUnitResultsConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialViewPolicyField' => 'DifferentialCoreCustomField', 'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'DiffusionAuditorsAddAuditorsHeraldAction' => 'DiffusionAuditorsHeraldAction', + 'DiffusionAuditorsAddSelfHeraldAction' => 'DiffusionAuditorsHeraldAction', + 'DiffusionAuditorsHeraldAction' => 'HeraldAction', + 'DiffusionBlockHeraldAction' => 'HeraldAction', 'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionBranchTableController' => 'DiffusionController', 'DiffusionBranchTableView' => 'DiffusionView', 'DiffusionBrowseController' => 'DiffusionController', 'DiffusionBrowseDirectoryController' => 'DiffusionBrowseController', 'DiffusionBrowseFileController' => 'DiffusionBrowseController', 'DiffusionBrowseMainController' => 'DiffusionBrowseController', 'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionBrowseResultSet' => 'Phobject', 'DiffusionBrowseSearchController' => 'DiffusionBrowseController', 'DiffusionBrowseTableView' => 'DiffusionView', 'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery', 'DiffusionChangeController' => 'DiffusionController', 'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup', 'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitBranchesController' => 'DiffusionController', 'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitChangeTableView' => 'DiffusionView', 'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitController' => 'DiffusionController', 'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffContentHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffContentRemovedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffEnormousHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitEditController' => 'DiffusionController', 'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitHash' => 'Phobject', 'DiffusionCommitHeraldField' => 'HeraldField', 'DiffusionCommitHeraldFieldGroup' => 'HeraldFieldGroup', 'DiffusionCommitHookEngine' => 'Phobject', 'DiffusionCommitHookRejectException' => 'Exception', 'DiffusionCommitMessageHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitPackageAuditHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitPackageHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitPackageOwnerHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DiffusionCommitRef' => 'Phobject', 'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase', 'DiffusionCommitRepositoryHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRepositoryProjectsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType', 'DiffusionCommitReviewerHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionAcceptedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionReviewersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionSubscribersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitTagsController' => 'DiffusionController', 'DiffusionConduitAPIMethod' => 'ConduitAPIMethod', 'DiffusionController' => 'PhabricatorController', 'DiffusionCreateCommentConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability', 'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability', 'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DiffusionDiffController' => 'DiffusionController', 'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery', 'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher', 'DiffusionEmptyResultView' => 'DiffusionView', 'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionExternalController' => 'DiffusionController', 'DiffusionExternalSymbolQuery' => 'Phobject', 'DiffusionExternalSymbolsSource' => 'Phobject', 'DiffusionFileContent' => 'Phobject', 'DiffusionFileContentQuery' => 'DiffusionQuery', 'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGetCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionGitBranch' => 'Phobject', 'DiffusionGitBranchTestCase' => 'PhabricatorTestCase', 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionGitFileContentQueryTestCase' => 'PhabricatorTestCase', 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionGitRequest' => 'DiffusionRequest', 'DiffusionGitResponse' => 'AphrontResponse', 'DiffusionGitSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionHistoryController' => 'DiffusionController', 'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionHistoryTableView' => 'DiffusionView', 'DiffusionHovercardEventListener' => 'PhabricatorEventListener', 'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController', 'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DiffusionLastModifiedController' => 'DiffusionController', 'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionLintController' => 'DiffusionController', 'DiffusionLintCountQuery' => 'PhabricatorQuery', 'DiffusionLintDetailsController' => 'DiffusionController', 'DiffusionLintSaveRunner' => 'Phobject', 'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelQuery' => 'Phobject', 'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery', 'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionMercurialRequest' => 'DiffusionRequest', 'DiffusionMercurialResponse' => 'AphrontResponse', 'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow', 'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel', 'DiffusionMercurialWireProtocol' => 'Phobject', 'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase', 'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionMirrorDeleteController' => 'DiffusionController', 'DiffusionMirrorEditController' => 'DiffusionController', 'DiffusionPathChange' => 'Phobject', 'DiffusionPathChangeQuery' => 'Phobject', 'DiffusionPathCompleteController' => 'DiffusionController', 'DiffusionPathIDQuery' => 'Phobject', 'DiffusionPathQuery' => 'Phobject', 'DiffusionPathQueryTestCase' => 'PhabricatorTestCase', 'DiffusionPathTreeController' => 'DiffusionController', 'DiffusionPathValidateController' => 'DiffusionController', 'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffEnormousHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentHeraldField' => 'HeraldField', 'DiffusionPreCommitContentMergeHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentMessageHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPusherHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentPusherProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRepositoryHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentRevisionAcceptedHeraldField' => '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', 'DiffusionPushCapability' => 'PhabricatorPolicyCapability', 'DiffusionPushEventViewController' => 'DiffusionPushLogController', 'DiffusionPushLogController' => 'DiffusionController', 'DiffusionPushLogListController' => 'DiffusionPushLogController', 'DiffusionPushLogListView' => 'AphrontView', 'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionQuery' => 'PhabricatorQuery', 'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionRawDiffQuery' => 'DiffusionQuery', 'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionReadmeView' => 'DiffusionView', 'DiffusionRefNotFoundException' => 'Exception', 'DiffusionRefTableController' => 'DiffusionController', 'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionRenameHistoryQuery' => 'Phobject', 'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource', 'DiffusionRepositoryDefaultController' => 'DiffusionController', 'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditController' => 'DiffusionController', 'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditEncodingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditHostingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditMainController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditStagingController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditStorageController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryListController' => 'DiffusionController', 'DiffusionRepositoryNewController' => 'DiffusionController', 'DiffusionRepositoryPath' => 'Phobject', 'DiffusionRepositoryRef' => 'Phobject', 'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionRepositorySymbolsController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryTag' => 'Phobject', 'DiffusionRequest' => 'Phobject', 'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionResolveUserQuery' => 'Phobject', 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionServeController' => 'DiffusionController', 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'AphrontUsageException', 'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow', 'DiffusionSubversionWireProtocol' => 'Phobject', 'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase', 'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionSvnRequest' => 'DiffusionRequest', 'DiffusionSymbolController' => 'DiffusionController', 'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource', 'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery', 'DiffusionTagListController' => 'DiffusionController', 'DiffusionTagListView' => 'DiffusionView', 'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionURITestCase' => 'PhutilTestCase', 'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod', 'DiffusionView' => 'AphrontView', 'DivinerArticleAtomizer' => 'DivinerAtomizer', 'DivinerAtom' => 'Phobject', 'DivinerAtomCache' => 'DivinerDiskCache', 'DivinerAtomController' => 'DivinerController', 'DivinerAtomListController' => 'DivinerController', 'DivinerAtomPHIDType' => 'PhabricatorPHIDType', 'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DivinerAtomRef' => 'Phobject', 'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DivinerAtomSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'DivinerAtomizeWorkflow' => 'DivinerWorkflow', 'DivinerAtomizer' => 'Phobject', 'DivinerBookController' => 'DivinerController', 'DivinerBookDatasource' => 'PhabricatorTypeaheadDatasource', 'DivinerBookEditController' => 'DivinerController', 'DivinerBookItemView' => 'AphrontTagView', 'DivinerBookPHIDType' => 'PhabricatorPHIDType', 'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DivinerBookSearchIndexer' => 'PhabricatorSearchDocumentIndexer', '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', ), 'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor', 'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction', 'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DivinerLivePublisher' => 'DivinerPublisher', 'DivinerLiveSymbol' => array( 'DivinerDAO', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorDestructibleInterface', ), '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', 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', 'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase', 'DoorkeeperDAO' => 'PhabricatorLiskDAO', 'DoorkeeperExternalObject' => array( 'DoorkeeperDAO', 'PhabricatorPolicyInterface', ), 'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DoorkeeperFeedStoryPublisher' => 'Phobject', 'DoorkeeperFeedWorker' => 'FeedPushWorker', 'DoorkeeperImportEngine' => 'Phobject', 'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker', 'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule', 'DoorkeeperMissingLinkException' => 'Exception', 'DoorkeeperObjectRef' => 'Phobject', 'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule', 'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAllocatorWorker' => 'PhabricatorWorker', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockBlueprint' => array( 'DrydockDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', ), 'DrydockBlueprintController' => 'DrydockController', 'DrydockBlueprintCoreCustomField' => array( 'DrydockBlueprintCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'DrydockBlueprintCreateController' => 'DrydockBlueprintController', 'DrydockBlueprintCustomField' => 'PhabricatorCustomField', 'DrydockBlueprintEditController' => 'DrydockBlueprintController', 'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor', 'DrydockBlueprintImplementation' => 'Phobject', 'DrydockBlueprintImplementationTestCase' => 'PhabricatorTestCase', 'DrydockBlueprintListController' => 'DrydockBlueprintController', 'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType', 'DrydockBlueprintQuery' => 'DrydockQuery', 'DrydockBlueprintScopeGuard' => 'Phobject', 'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction', 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DrydockBlueprintViewController' => 'DrydockBlueprintController', 'DrydockCommandInterface' => 'DrydockInterface', 'DrydockConsoleController' => 'DrydockController', 'DrydockConstants' => 'Phobject', 'DrydockController' => 'PhabricatorController', 'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability', 'DrydockDAO' => 'PhabricatorLiskDAO', 'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DrydockFilesystemInterface' => 'DrydockInterface', 'DrydockInterface' => 'Phobject', 'DrydockLease' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockLeaseController' => 'DrydockController', 'DrydockLeaseListController' => 'DrydockLeaseController', 'DrydockLeaseListView' => 'AphrontView', 'DrydockLeasePHIDType' => 'PhabricatorPHIDType', 'DrydockLeaseQuery' => 'DrydockQuery', 'DrydockLeaseReleaseController' => 'DrydockLeaseController', 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'DrydockConstants', 'DrydockLeaseViewController' => 'DrydockLeaseController', 'DrydockLocalCommandInterface' => 'DrydockCommandInterface', 'DrydockLog' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockLogController' => 'DrydockController', 'DrydockLogListController' => 'DrydockLogController', 'DrydockLogListView' => 'AphrontView', 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementCreateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), 'DrydockResourceCloseController' => 'DrydockResourceController', 'DrydockResourceController' => 'DrydockController', 'DrydockResourceListController' => 'DrydockResourceController', 'DrydockResourceListView' => 'AphrontView', 'DrydockResourcePHIDType' => 'PhabricatorPHIDType', 'DrydockResourceQuery' => 'DrydockQuery', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'DrydockConstants', 'DrydockResourceViewController' => 'DrydockResourceController', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'DrydockWebrootInterface' => 'DrydockInterface', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', 'FeedPublisherHTTPWorker' => 'FeedPushWorker', 'FeedPublisherWorker' => 'FeedPushWorker', 'FeedPushWorker' => 'PhabricatorWorker', 'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod', 'FeedStoryNotificationGarbageCollector' => 'PhabricatorGarbageCollector', 'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod', 'FileConduitAPIMethod' => 'ConduitAPIMethod', 'FileCreateMailReceiver' => 'PhabricatorMailReceiver', 'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod', 'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod', 'FileMailReceiver' => 'PhabricatorObjectMailReceiver', 'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod', 'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', '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', 'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine', 'FundBackerTransaction' => 'PhabricatorApplicationTransaction', 'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'FundController' => 'PhabricatorController', 'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability', 'FundDAO' => 'PhabricatorLiskDAO', 'FundDefaultViewCapability' => 'PhabricatorPolicyCapability', 'FundInitiative' => array( 'FundDAO', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorMentionableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', ), 'FundInitiativeBackController' => 'FundController', 'FundInitiativeCloseController' => 'FundController', 'FundInitiativeEditController' => 'FundController', 'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor', 'FundInitiativeIndexer' => 'PhabricatorSearchDocumentIndexer', 'FundInitiativeListController' => 'FundController', 'FundInitiativePHIDType' => 'PhabricatorPHIDType', 'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine', 'FundInitiativeTransaction' => 'PhabricatorApplicationTransaction', 'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'FundInitiativeViewController' => 'FundController', 'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase', 'HarbormasterBuild' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'HarbormasterBuildAbortedException' => 'Exception', 'HarbormasterBuildActionController' => 'HarbormasterController', 'HarbormasterBuildArcanistAutoplan' => 'HarbormasterBuildAutoplan', 'HarbormasterBuildArtifact' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', ), 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildAutoplan' => 'Phobject', 'HarbormasterBuildCommand' => 'HarbormasterDAO', 'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildEngine' => 'Phobject', 'HarbormasterBuildFailureException' => 'Exception', 'HarbormasterBuildGraph' => 'AbstractDirectedGraph', 'HarbormasterBuildLintMessage' => 'HarbormasterDAO', 'HarbormasterBuildLog' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', ), 'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildMessage' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', ), 'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildPlan' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', ), 'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', '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', ), 'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildUnitMessage' => 'HarbormasterDAO', 'HarbormasterBuildViewController' => 'HarbormasterController', 'HarbormasterBuildWorker' => 'HarbormasterWorker', 'HarbormasterBuildable' => array( 'HarbormasterDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'HarbormasterBuildableInterface', ), 'HarbormasterBuildableActionController' => 'HarbormasterController', 'HarbormasterBuildableListController' => 'HarbormasterController', 'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildableViewController' => 'HarbormasterController', + 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod', 'HarbormasterController' => 'PhabricatorController', 'HarbormasterDAO' => 'PhabricatorLiskDAO', + 'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintPropertyView' => 'AphrontView', 'HarbormasterManagePlansCapability' => 'PhabricatorPolicyCapability', 'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'HarbormasterMessageType' => 'Phobject', 'HarbormasterObject' => 'HarbormasterDAO', + 'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterPlanController' => 'HarbormasterController', 'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController', 'HarbormasterPlanListController' => 'HarbormasterPlanController', 'HarbormasterPlanRunController' => 'HarbormasterController', 'HarbormasterPlanViewController' => 'HarbormasterPlanController', + 'HarbormasterPrototypeBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterQueryAutotargetsConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction', 'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HarbormasterScratchTable' => 'HarbormasterDAO', 'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterStepAddController' => 'HarbormasterController', 'HarbormasterStepDeleteController' => 'HarbormasterController', 'HarbormasterStepEditController' => 'HarbormasterController', 'HarbormasterTargetEngine' => 'Phobject', 'HarbormasterTargetWorker' => 'HarbormasterWorker', + 'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation', 'HarbormasterUIEventListener' => 'PhabricatorEventListener', 'HarbormasterUnitMessagesController' => 'HarbormasterController', 'HarbormasterUnitPropertyView' => 'AphrontView', 'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWorker' => 'PhabricatorWorker', + 'HeraldAction' => 'Phobject', + 'HeraldActionGroup' => 'HeraldGroup', 'HeraldActionRecord' => 'HeraldDAO', 'HeraldAdapter' => 'Phobject', 'HeraldAlwaysField' => 'HeraldField', 'HeraldAnotherRuleField' => 'HeraldField', + 'HeraldApplicationActionGroup' => 'HeraldActionGroup', 'HeraldApplyTranscript' => 'Phobject', 'HeraldBasicFieldGroup' => 'HeraldFieldGroup', - 'HeraldCommitAdapter' => 'HeraldAdapter', + 'HeraldCommitAdapter' => array( + 'HeraldAdapter', + 'HarbormasterBuildableAdapterInterface', + ), 'HeraldCondition' => 'HeraldDAO', 'HeraldConditionTranscript' => 'Phobject', 'HeraldContentSourceField' => 'HeraldField', 'HeraldController' => 'PhabricatorController', - 'HeraldCustomAction' => 'Phobject', 'HeraldDAO' => 'PhabricatorLiskDAO', 'HeraldDifferentialAdapter' => 'HeraldAdapter', 'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter', - 'HeraldDifferentialRevisionAdapter' => 'HeraldDifferentialAdapter', + 'HeraldDifferentialRevisionAdapter' => array( + 'HeraldDifferentialAdapter', + 'HarbormasterBuildableAdapterInterface', + ), 'HeraldDisableController' => 'HeraldController', + 'HeraldDoNothingAction' => 'HeraldAction', 'HeraldEditFieldGroup' => 'HeraldFieldGroup', 'HeraldEffect' => 'Phobject', 'HeraldEmptyFieldValue' => 'HeraldFieldValue', 'HeraldEngine' => 'Phobject', 'HeraldField' => 'Phobject', - 'HeraldFieldGroup' => 'Phobject', + 'HeraldFieldGroup' => 'HeraldGroup', 'HeraldFieldTestCase' => 'PhutilTestCase', 'HeraldFieldValue' => 'Phobject', + 'HeraldGroup' => 'Phobject', 'HeraldInvalidActionException' => 'Exception', 'HeraldInvalidConditionException' => 'Exception', 'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability', 'HeraldManiphestTaskAdapter' => 'HeraldAdapter', 'HeraldNewController' => 'HeraldController', 'HeraldNewObjectField' => 'HeraldField', + 'HeraldNotifyActionGroup' => 'HeraldActionGroup', 'HeraldObjectTranscript' => 'Phobject', 'HeraldPholioMockAdapter' => 'HeraldAdapter', 'HeraldPreCommitAdapter' => 'HeraldAdapter', 'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter', 'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter', + 'HeraldPreventActionGroup' => 'HeraldActionGroup', 'HeraldProjectsField' => 'HeraldField', 'HeraldRecursiveConditionsException' => 'Exception', 'HeraldRelatedFieldGroup' => 'HeraldFieldGroup', 'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'HeraldRepetitionPolicyConfig' => 'Phobject', 'HeraldRule' => array( 'HeraldDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'HeraldRuleController' => 'HeraldController', 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldRuleListController' => 'HeraldController', 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldRuleTestCase' => 'PhabricatorTestCase', 'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction', 'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment', 'HeraldRuleTranscript' => 'Phobject', 'HeraldRuleTypeConfig' => 'Phobject', 'HeraldRuleViewController' => 'HeraldController', 'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HeraldSelectFieldValue' => 'HeraldFieldValue', 'HeraldSpaceField' => 'HeraldField', 'HeraldSubscribersField' => 'HeraldField', + 'HeraldSupportActionGroup' => 'HeraldActionGroup', 'HeraldSupportFieldGroup' => 'HeraldFieldGroup', 'HeraldTestConsoleController' => 'HeraldController', 'HeraldTextFieldValue' => 'HeraldFieldValue', 'HeraldTokenizerFieldValue' => 'HeraldFieldValue', 'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HeraldTranscript' => array( 'HeraldDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'HeraldTranscriptController' => 'HeraldController', 'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector', 'HeraldTranscriptListController' => 'HeraldController', 'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldTranscriptTestCase' => 'PhabricatorTestCase', + 'HeraldUtilityActionGroup' => 'HeraldActionGroup', 'Javelin' => 'Phobject', 'JavelinReactorUIExample' => 'PhabricatorUIExample', 'JavelinUIExample' => 'PhabricatorUIExample', 'JavelinViewExampleServerView' => 'AphrontView', 'JavelinViewUIExample' => 'PhabricatorUIExample', 'LegalpadController' => 'PhabricatorController', 'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability', 'LegalpadDAO' => 'PhabricatorLiskDAO', 'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability', 'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability', 'LegalpadDocument' => array( 'LegalpadDAO', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'LegalpadDocumentBody' => array( 'LegalpadDAO', 'PhabricatorMarkupInterface', ), 'LegalpadDocumentCommentController' => 'LegalpadController', 'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource', 'LegalpadDocumentDoneController' => 'LegalpadController', 'LegalpadDocumentEditController' => 'LegalpadController', 'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor', 'LegalpadDocumentListController' => 'LegalpadController', 'LegalpadDocumentManageController' => 'LegalpadController', 'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'LegalpadDocumentSignController' => 'LegalpadController', 'LegalpadDocumentSignature' => array( 'LegalpadDAO', 'PhabricatorPolicyInterface', ), 'LegalpadDocumentSignatureAddController' => 'LegalpadController', 'LegalpadDocumentSignatureListController' => 'LegalpadController', 'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine', 'LegalpadDocumentSignatureVerificationController' => 'LegalpadController', 'LegalpadDocumentSignatureViewController' => 'LegalpadController', 'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver', 'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType', 'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'LegalpadRequireSignatureHeraldAction' => 'HeraldAction', 'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType', 'LegalpadTransaction' => 'PhabricatorApplicationTransaction', 'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment', 'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView', 'LiskChunkTestCase' => 'PhabricatorTestCase', 'LiskDAO' => 'Phobject', 'LiskDAOSet' => 'Phobject', 'LiskDAOTestCase' => 'PhabricatorTestCase', 'LiskEphemeralObjectException' => 'Exception', 'LiskFixtureTestCase' => 'PhabricatorTestCase', 'LiskIsolationTestCase' => 'PhabricatorTestCase', 'LiskIsolationTestDAO' => 'LiskDAO', 'LiskIsolationTestDAOException' => 'Exception', 'LiskMigrationIterator' => 'PhutilBufferedIterator', 'LiskRawMigrationIterator' => 'PhutilBufferedIterator', 'MacroConduitAPIMethod' => 'ConduitAPIMethod', 'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod', 'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod', 'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand', 'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'ManiphestBatchEditController' => 'ManiphestController', 'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability', 'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand', 'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand', 'ManiphestConduitAPIMethod' => 'ConduitAPIMethod', 'ManiphestConfiguredCustomField' => array( 'ManiphestCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'ManiphestConstants' => 'Phobject', 'ManiphestController' => 'PhabricatorController', 'ManiphestCreateMailReceiver' => 'PhabricatorMailReceiver', 'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestCustomField' => 'PhabricatorCustomField', 'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser', 'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase', 'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability', 'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditAssignCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditPoliciesCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditPriorityCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditProjectsCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditStatusCapability' => 'PhabricatorPolicyCapability', 'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand', 'ManiphestExcelDefaultFormat' => 'ManiphestExcelFormat', 'ManiphestExcelFormat' => 'Phobject', 'ManiphestExcelFormatTestCase' => 'PhabricatorTestCase', 'ManiphestExportController' => 'ManiphestController', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestHovercardEventListener' => 'PhabricatorEventListener', 'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestNameIndex' => 'ManiphestDAO', 'ManiphestNameIndexEventListener' => 'PhabricatorEventListener', 'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand', 'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'ManiphestReportController' => 'ManiphestController', 'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ManiphestSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'ManiphestStatusConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand', 'ManiphestSubpriorityController' => 'ManiphestController', 'ManiphestTask' => array( 'ManiphestDAO', 'PhabricatorSubscribableInterface', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorMentionableInterface', 'PhrequentTrackableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorSpacesInterface', ), + 'ManiphestTaskAssignHeraldAction' => 'HeraldAction', + 'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction', + 'ManiphestTaskAssignSelfHeraldAction' => 'ManiphestTaskAssignHeraldAction', 'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule', 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDescriptionHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskDetailController' => 'ManiphestController', 'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType', 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHeraldField' => 'HeraldField', 'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup', 'ManiphestTaskListController' => 'ManiphestController', 'ManiphestTaskListView' => 'ManiphestView', 'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver', 'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', 'ManiphestTaskPriority' => 'ManiphestConstants', 'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPriorityHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ManiphestTaskResultListView' => 'ManiphestView', 'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ManiphestTaskStatus' => 'ManiphestConstants', 'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'ManiphestTaskStatusHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase', 'ManiphestTaskTestCase' => 'PhabricatorTestCase', 'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTransaction' => 'PhabricatorApplicationTransaction', 'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'ManiphestTransactionPreviewController' => 'ManiphestController', 'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ManiphestTransactionSaveController' => 'ManiphestController', 'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestView' => 'AphrontView', 'MetaMTAConstants' => 'Phobject', 'MetaMTAEmailTransactionCommand' => 'Phobject', 'MetaMTAEmailTransactionCommandTestCase' => 'PhabricatorTestCase', 'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector', 'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector', 'MetaMTAReceivedMailStatus' => 'MetaMTAConstants', 'MultimeterContext' => 'MultimeterDimension', 'MultimeterControl' => 'Phobject', 'MultimeterController' => 'PhabricatorController', 'MultimeterDAO' => 'PhabricatorLiskDAO', 'MultimeterDimension' => 'MultimeterDAO', 'MultimeterEvent' => 'MultimeterDAO', 'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector', 'MultimeterHost' => 'MultimeterDimension', 'MultimeterLabel' => 'MultimeterDimension', 'MultimeterSampleController' => 'MultimeterController', 'MultimeterViewer' => 'MultimeterDimension', 'NuanceConduitAPIMethod' => 'ConduitAPIMethod', 'NuanceController' => 'PhabricatorController', 'NuanceCreateItemConduitAPIMethod' => 'NuanceConduitAPIMethod', 'NuanceDAO' => 'PhabricatorLiskDAO', 'NuanceItem' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'NuanceItemEditController' => 'NuanceController', 'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceItemPHIDType' => 'PhabricatorPHIDType', 'NuanceItemQuery' => 'NuanceQuery', 'NuanceItemTransaction' => 'NuanceTransaction', 'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceItemViewController' => 'NuanceController', 'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition', 'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'NuanceQueue' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'NuanceQueueEditController' => 'NuanceController', 'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceQueueItem' => 'NuanceDAO', 'NuanceQueueListController' => 'NuanceController', 'NuanceQueuePHIDType' => 'PhabricatorPHIDType', 'NuanceQueueQuery' => 'NuanceQuery', 'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceQueueTransaction' => 'NuanceTransaction', 'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceQueueViewController' => 'NuanceController', 'NuanceRequestor' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'NuanceRequestorEditController' => 'NuanceController', 'NuanceRequestorEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceRequestorPHIDType' => 'PhabricatorPHIDType', 'NuanceRequestorQuery' => 'NuanceQuery', 'NuanceRequestorSource' => 'NuanceDAO', 'NuanceRequestorTransaction' => 'NuanceTransaction', 'NuanceRequestorTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceRequestorTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceRequestorViewController' => 'NuanceController', 'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'NuanceSource' => array( 'NuanceDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'NuanceSourceActionController' => 'NuanceController', 'NuanceSourceCreateController' => 'NuanceController', 'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceDefinition' => 'Phobject', 'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase', 'NuanceSourceEditController' => 'NuanceController', 'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceSourceListController' => 'NuanceController', 'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability', 'NuanceSourcePHIDType' => 'PhabricatorPHIDType', 'NuanceSourceQuery' => 'NuanceQuery', 'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceSourceTransaction' => 'NuanceTransaction', 'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'NuanceSourceViewController' => 'NuanceController', 'NuanceTransaction' => 'PhabricatorApplicationTransaction', 'OwnersConduitAPIMethod' => 'ConduitAPIMethod', 'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler', 'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod', 'PHIDConduitAPIMethod' => 'ConduitAPIMethod', 'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod', 'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod', 'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod', 'PHUI' => 'Phobject', 'PHUIActionPanelExample' => 'PhabricatorUIExample', 'PHUIActionPanelView' => 'AphrontTagView', 'PHUIBadgeBoxView' => 'AphrontTagView', 'PHUIBadgeExample' => 'PhabricatorUIExample', 'PHUIBadgeMiniView' => 'AphrontTagView', 'PHUIBadgeView' => 'AphrontTagView', 'PHUIBoxExample' => 'PhabricatorUIExample', 'PHUIBoxView' => 'AphrontTagView', 'PHUIButtonBarExample' => 'PhabricatorUIExample', 'PHUIButtonBarView' => 'AphrontTagView', 'PHUIButtonExample' => 'PhabricatorUIExample', 'PHUIButtonView' => 'AphrontTagView', 'PHUICalendarDayView' => 'AphrontView', 'PHUICalendarListView' => 'AphrontTagView', 'PHUICalendarMonthView' => 'AphrontView', 'PHUICalendarWidgetView' => 'AphrontTagView', 'PHUIColorPalletteExample' => 'PhabricatorUIExample', 'PHUICrumbView' => 'AphrontView', 'PHUICrumbsView' => 'AphrontView', 'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentRowScaffold' => 'AphrontView', 'PHUIDiffInlineCommentTableScaffold' => 'AphrontView', 'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentView' => 'AphrontView', 'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold', 'PHUIDiffRevealIconView' => 'AphrontView', 'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold', 'PHUIDocumentExample' => 'PhabricatorUIExample', 'PHUIDocumentView' => 'AphrontTagView', 'PHUIFeedStoryExample' => 'PhabricatorUIExample', 'PHUIFeedStoryView' => 'AphrontView', 'PHUIFormDividerControl' => 'AphrontFormControl', 'PHUIFormFreeformDateControl' => 'AphrontFormControl', 'PHUIFormInsetView' => 'AphrontView', 'PHUIFormLayoutView' => 'AphrontView', 'PHUIFormMultiSubmitControl' => 'AphrontFormControl', 'PHUIFormPageView' => 'AphrontView', 'PHUIHandleListView' => 'AphrontTagView', 'PHUIHandleTagListView' => 'AphrontTagView', 'PHUIHandleView' => 'AphrontView', 'PHUIHeaderView' => 'AphrontTagView', 'PHUIIconExample' => 'PhabricatorUIExample', 'PHUIIconView' => 'AphrontTagView', 'PHUIImageMaskExample' => 'PhabricatorUIExample', 'PHUIImageMaskView' => 'AphrontTagView', 'PHUIInfoExample' => 'PhabricatorUIExample', 'PHUIInfoPanelExample' => 'PhabricatorUIExample', 'PHUIInfoPanelView' => 'AphrontView', 'PHUIInfoView' => 'AphrontView', 'PHUIListExample' => 'PhabricatorUIExample', 'PHUIListItemView' => 'AphrontTagView', 'PHUIListView' => 'AphrontTagView', 'PHUIListViewTestCase' => 'PhabricatorTestCase', 'PHUIObjectBoxView' => 'AphrontView', 'PHUIObjectItemListExample' => 'PhabricatorUIExample', 'PHUIObjectItemListView' => 'AphrontTagView', 'PHUIObjectItemView' => 'AphrontTagView', 'PHUIPagedFormView' => 'AphrontView', 'PHUIPagerView' => 'AphrontView', 'PHUIPinboardItemView' => 'AphrontView', 'PHUIPinboardView' => 'AphrontView', 'PHUIPropertyGroupView' => 'AphrontTagView', 'PHUIPropertyListExample' => 'PhabricatorUIExample', 'PHUIPropertyListView' => 'AphrontView', 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', 'PHUISpacesNamespaceContextView' => 'AphrontView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', 'PHUITagExample' => 'PhabricatorUIExample', 'PHUITagView' => 'AphrontTagView', 'PHUITextExample' => 'PhabricatorUIExample', 'PHUITextView' => 'AphrontTagView', 'PHUITimelineEventView' => 'AphrontView', 'PHUITimelineExample' => 'PhabricatorUIExample', 'PHUITimelineView' => 'AphrontView', 'PHUITypeaheadExample' => 'PhabricatorUIExample', 'PHUIWorkboardView' => 'AphrontTagView', 'PHUIWorkpanelView' => 'AphrontTagView', 'PassphraseAbstractKey' => 'Phobject', 'PassphraseConduitAPIMethod' => 'ConduitAPIMethod', 'PassphraseController' => 'PhabricatorController', 'PassphraseCredential' => array( 'PassphraseDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', + 'PhabricatorSubscribableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', ), 'PassphraseCredentialAuthorPolicyRule' => 'PhabricatorPolicyRule', 'PassphraseCredentialConduitController' => 'PassphraseController', 'PassphraseCredentialControl' => 'AphrontFormControl', 'PassphraseCredentialCreateController' => 'PassphraseController', 'PassphraseCredentialDestroyController' => 'PassphraseController', 'PassphraseCredentialEditController' => 'PassphraseController', 'PassphraseCredentialListController' => 'PassphraseController', 'PassphraseCredentialLockController' => 'PassphraseController', 'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType', 'PassphraseCredentialPublicController' => 'PassphraseController', 'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PassphraseCredentialRevealController' => 'PassphraseController', 'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PassphraseCredentialTransaction' => 'PhabricatorApplicationTransaction', 'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PassphraseCredentialType' => 'Phobject', 'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase', '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', 'PassphraseSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PassphraseSecret' => 'PassphraseDAO', 'PasteConduitAPIMethod' => 'ConduitAPIMethod', 'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', 'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PasteEmbedView' => 'AphrontView', 'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteMailReceiver' => 'PhabricatorObjectMailReceiver', 'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability', 'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability', 'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase', 'PhabricatorAccessLog' => 'Phobject', 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccountSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorActionListView' => 'AphrontView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorAlmanacApplication' => 'PhabricatorApplication', 'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorAnchorView' => 'AphrontView', 'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample', 'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase', 'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorApplication' => array( 'Phobject', 'PhabricatorPolicyInterface', ), 'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType', 'PhabricatorApplicationConfigOptions' => 'Phobject', 'PhabricatorApplicationConfigurationPanel' => 'Phobject', 'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase', 'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationLaunchView' => 'AphrontTagView', 'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorApplicationSearchEngine' => 'Phobject', 'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorApplicationSearchResultView' => 'Phobject', 'PhabricatorApplicationStatusView' => 'AphrontView', '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', 'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionStructureException' => 'Exception', 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorApplicationTransactionValidationError' => 'Phobject', 'PhabricatorApplicationTransactionValidationException' => 'Exception', 'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionView' => 'AphrontView', 'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationsApplication' => 'PhabricatorApplication', 'PhabricatorApplicationsController' => 'PhabricatorController', 'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController', 'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAuditActionConstants' => 'Phobject', 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', 'PhabricatorAuditApplication' => 'PhabricatorApplication', 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', 'PhabricatorAuditCommitStatusConstants' => 'Phobject', 'PhabricatorAuditController' => 'PhabricatorController', 'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuditInlineComment' => array( 'Phobject', 'PhabricatorInlineCommentInterface', ), 'PhabricatorAuditListController' => 'PhabricatorAuditController', 'PhabricatorAuditListView' => 'AphrontView', 'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow', 'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAuditPreviewController' => 'PhabricatorAuditController', 'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorAuditStatusConstants' => 'Phobject', 'PhabricatorAuditTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView', 'PhabricatorAuthAccountView' => 'AphrontView', 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorAuthDAO' => 'PhabricatorLiskDAO', 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', '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', 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController', 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthProvider' => 'Phobject', 'PhabricatorAuthProviderConfig' => array( 'PhabricatorAuthDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController', 'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController', 'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController', 'PhabricatorAuthSSHKey' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController', 'PhabricatorAuthSSHKeyDeleteController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController', 'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSSHKeyTableView' => 'AphrontView', 'PhabricatorAuthSSHPublicKey' => 'Phobject', 'PhabricatorAuthSession' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthSessionEngine' => 'Phobject', 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 'PhabricatorAuthTemporaryToken' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAutoEventListener' => 'PhabricatorEventListener', 'PhabricatorBadgeHasRecipientEdgeType' => 'PhabricatorEdgeType', 'PhabricatorBadgesApplication' => 'PhabricatorApplication', 'PhabricatorBadgesBadge' => array( 'PhabricatorBadgesDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', + 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorBadgesCommentController' => 'PhabricatorBadgesController', 'PhabricatorBadgesController' => 'PhabricatorController', 'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability', 'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO', 'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorBadgesEditController' => 'PhabricatorBadgesController', 'PhabricatorBadgesEditIconController' => 'PhabricatorBadgesController', 'PhabricatorBadgesEditRecipientsController' => 'PhabricatorBadgesController', 'PhabricatorBadgesEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorBadgesIcon' => 'Phobject', 'PhabricatorBadgesListController' => 'PhabricatorBadgesController', + 'PhabricatorBadgesMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorBadgesPHIDType' => 'PhabricatorPHIDType', 'PhabricatorBadgesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorBadgesRecipientsListView' => 'AphrontTagView', 'PhabricatorBadgesRemoveRecipientsController' => 'PhabricatorBadgesController', + 'PhabricatorBadgesReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorBadgesSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorBadgesSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorBadgesTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorBadgesTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorBadgesTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorBadgesViewController' => 'PhabricatorBadgesController', 'PhabricatorBarePageUIExample' => 'PhabricatorUIExample', 'PhabricatorBarePageView' => 'AphrontPageView', 'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', 'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler', 'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter', 'PhabricatorBotHandler' => 'Phobject', 'PhabricatorBotLogHandler' => 'PhabricatorBotHandler', 'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler', 'PhabricatorBotMessage' => 'Phobject', 'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler', 'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler', 'PhabricatorBotTarget' => 'Phobject', 'PhabricatorBotUser' => 'PhabricatorBotTarget', 'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler', 'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation', 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', 'PhabricatorBusyUIExample' => 'PhabricatorUIExample', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', 'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow', 'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorCacheSpec' => 'Phobject', 'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorCaches' => 'Phobject', 'PhabricatorCachesTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarApplication' => 'PhabricatorApplication', 'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', 'PhabricatorCalendarEvent' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorMarkupInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorMentionableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditIconController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorCalendarEventInvitee' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand', 'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarEventSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorCalendarEventTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarIcon' => 'Phobject', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCampfireProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter', 'PhabricatorCelerityApplication' => 'PhabricatorApplication', 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChatLogApplication' => 'PhabricatorApplication', 'PhabricatorChatLogChannel' => array( 'PhabricatorChatLogDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorChatLogController' => 'PhabricatorController', 'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO', 'PhabricatorChatLogEvent' => array( 'PhabricatorChatLogDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField', 'PhabricatorCommitCustomField' => 'PhabricatorCustomField', 'PhabricatorCommitMergedCommitsField' => 'PhabricatorCommitCustomField', 'PhabricatorCommitRepositoryField' => 'PhabricatorCommitCustomField', 'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField', 'PhabricatorCommonPasswords' => 'Phobject', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitApplication' => 'PhabricatorApplication', 'PhabricatorConduitCertificateSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitController' => 'PhabricatorController', 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', 'PhabricatorConduitListController' => 'PhabricatorConduitController', 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitMethodCallLog' => array( 'PhabricatorConduitDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorConduitTestCase' => 'PhabricatorTestCase', 'PhabricatorConduitToken' => array( 'PhabricatorConduitDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorConduitTokenController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController', 'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController', 'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorConfigAllController' => 'PhabricatorConfigController', 'PhabricatorConfigApplication' => 'PhabricatorApplication', 'PhabricatorConfigCacheController' => 'PhabricatorConfigController', 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', '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', 'PhabricatorConfigGroupController' => 'PhabricatorConfigController', 'PhabricatorConfigHistoryController' => 'PhabricatorConfigController', 'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController', 'PhabricatorConfigIssueListController' => 'PhabricatorConfigController', 'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController', 'PhabricatorConfigJSON' => 'Phobject', 'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType', 'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigListController' => 'PhabricatorConfigController', 'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow', 'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorConfigModule' => 'Phobject', 'PhabricatorConfigModuleController' => 'PhabricatorConfigController', 'PhabricatorConfigOption' => array( 'Phobject', 'PhabricatorMarkupInterface', ), 'PhabricatorConfigOptionType' => 'Phobject', 'PhabricatorConfigPHIDModule' => 'PhabricatorConfigModule', 'PhabricatorConfigProxySource' => 'PhabricatorConfigSource', 'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse', 'PhabricatorConfigSchemaQuery' => 'Phobject', 'PhabricatorConfigSchemaSpec' => 'Phobject', 'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigSiteModule' => 'PhabricatorConfigModule', 'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigSource' => 'Phobject', 'PhabricatorConfigStackSource' => 'PhabricatorConfigSource', 'PhabricatorConfigStorageSchema' => 'Phobject', 'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorConfigValidationException' => 'Exception', 'PhabricatorConfigWelcomeController' => 'PhabricatorConfigController', 'PhabricatorConpherenceApplication' => 'PhabricatorApplication', 'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType', 'PhabricatorConsoleApplication' => 'PhabricatorApplication', 'PhabricatorContentSource' => 'Phobject', 'PhabricatorContentSourceView' => 'AphrontView', 'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorController' => 'AphrontController', 'PhabricatorCookies' => 'Phobject', 'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorCountdown' => array( 'PhabricatorCountdownDAO', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSpacesInterface', 'PhabricatorProjectInterface', ), 'PhabricatorCountdownApplication' => 'PhabricatorApplication', 'PhabricatorCountdownCommentController' => 'PhabricatorCountdownController', 'PhabricatorCountdownController' => 'PhabricatorController', 'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO', 'PhabricatorCountdownDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController', 'PhabricatorCountdownEditController' => 'PhabricatorCountdownController', 'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCountdownListController' => 'PhabricatorCountdownController', + 'PhabricatorCountdownMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCountdownReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCountdownSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCountdownTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorCountdownTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCountdownView' => 'AphrontTagView', 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCredentialsUsedByObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorCustomField' => 'Phobject', 'PhabricatorCustomFieldAttachment' => 'Phobject', 'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType', 'PhabricatorCustomFieldDataNotAvailableException' => 'Exception', 'PhabricatorCustomFieldHeraldField' => 'HeraldField', 'PhabricatorCustomFieldHeraldFieldGroup' => 'HeraldFieldGroup', 'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception', 'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO', 'PhabricatorCustomFieldList' => 'Phobject', 'PhabricatorCustomFieldMonogramParser' => 'Phobject', 'PhabricatorCustomFieldNotAttachedException' => 'Exception', 'PhabricatorCustomFieldNotProxyException' => 'Exception', 'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO', 'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomHeaderConfigType' => 'PhabricatorConfigOptionType', 'PhabricatorDaemon' => 'PhutilDaemon', 'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonController', 'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonController', 'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonController', 'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController', 'PhabricatorDaemonController' => 'PhabricatorController', 'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO', 'PhabricatorDaemonEventListener' => 'PhabricatorEventListener', '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', 'PhabricatorDaemonReference' => 'Phobject', 'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonTasksTableView' => 'AphrontView', 'PhabricatorDaemonsApplication' => 'PhabricatorApplication', 'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorDashboard' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', ), 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 'PhabricatorDashboardController' => 'PhabricatorController', 'PhabricatorDashboardCopyController' => 'PhabricatorDashboardController', 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', 'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType', 'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType', 'PhabricatorDashboardEditController' => 'PhabricatorDashboardController', 'PhabricatorDashboardHistoryController' => 'PhabricatorDashboardController', 'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO', 'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardLayoutConfig' => 'Phobject', 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', 'PhabricatorDashboardManageController' => 'PhabricatorDashboardController', 'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanel' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelCoreCustomField' => array( 'PhabricatorDashboardPanelCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField', 'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType', 'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController', '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', 'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardRenderingEngine' => 'Phobject', 'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDashboardUninstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardViewController' => 'PhabricatorDashboardController', 'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorDataNotAttachedException' => 'Exception', 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDesktopNotificationsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDifferenceEngine' => 'Phobject', 'PhabricatorDifferentialApplication' => 'PhabricatorApplication', 'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorDiffusionApplication' => 'PhabricatorApplication', 'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDisabledUserController' => 'PhabricatorAuthController', 'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorDisqusConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDivinerApplication' => 'PhabricatorApplication', 'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorDrydockApplication' => 'PhabricatorApplication', 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 'PhabricatorEdgeConstants' => 'Phobject', 'PhabricatorEdgeCycleException' => 'Exception', 'PhabricatorEdgeEditor' => 'Phobject', 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeType' => 'Phobject', 'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase', 'PhabricatorEditor' => 'Phobject', 'PhabricatorElasticSearchEngine' => 'PhabricatorSearchEngine', 'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailVerificationController' => 'PhabricatorAuthController', 'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorEmptyQueryException' => 'Exception', 'PhabricatorEnv' => 'Phobject', 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', 'PhabricatorEvent' => 'PhutilEvent', 'PhabricatorEventEngine' => 'Phobject', 'PhabricatorEventListener' => 'PhutilEventListener', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', 'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorExternalAccount' => array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorFactAggregate' => 'PhabricatorFactDAO', 'PhabricatorFactApplication' => 'PhabricatorApplication', 'PhabricatorFactChartController' => 'PhabricatorFactController', 'PhabricatorFactController' => 'PhabricatorController', 'PhabricatorFactCountEngine' => 'PhabricatorFactEngine', 'PhabricatorFactCursor' => 'PhabricatorFactDAO', 'PhabricatorFactDAO' => 'PhabricatorLiskDAO', 'PhabricatorFactDaemon' => 'PhabricatorDaemon', 'PhabricatorFactEngine' => 'Phobject', 'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFactHomeController' => 'PhabricatorFactController', 'PhabricatorFactLastUpdatedEngine' => 'PhabricatorFactEngine', 'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec', 'PhabricatorFactSpec' => 'Phobject', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', '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' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryPublisher' => 'Phobject', 'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO', 'PhabricatorFile' => array( 'PhabricatorFileDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorFileBundleLoader' => 'Phobject', 'PhabricatorFileChunk' => array( 'PhabricatorFileDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorFileChunkIterator' => array( 'Phobject', 'Iterator', ), 'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFileCommentController' => 'PhabricatorFileController', 'PhabricatorFileComposeController' => 'PhabricatorFileController', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileDataController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', 'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType', 'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorFileImageMacro' => array( 'PhabricatorFileDAO', 'PhabricatorSubscribableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', + 'PhabricatorTokenReceiverInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', 'PhabricatorFileInfoController' => 'PhabricatorFileController', 'PhabricatorFileLinkListView' => 'AphrontView', 'PhabricatorFileLinkView' => 'AphrontView', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileStorageConfigurationException' => 'Exception', 'PhabricatorFileStorageEngine' => 'Phobject', 'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorFileTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform', 'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorFileTransform' => 'Phobject', 'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileTransformListController' => 'PhabricatorFileController', 'PhabricatorFileTransformTestCase' => 'PhabricatorTestCase', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadDialogController' => 'PhabricatorFileController', 'PhabricatorFileUploadException' => 'Exception', 'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFilesApplication' => 'PhabricatorApplication', 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction', 'PhabricatorFlag' => array( 'PhabricatorFlagDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction', 'PhabricatorFlagColor' => 'PhabricatorFlagConstants', 'PhabricatorFlagConstants' => 'Phobject', 'PhabricatorFlagController' => 'PhabricatorController', 'PhabricatorFlagDAO' => 'PhabricatorLiskDAO', 'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', 'PhabricatorFlagEditController' => 'PhabricatorFlagController', 'PhabricatorFlagListController' => 'PhabricatorFlagController', 'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFlagSelectControl' => 'AphrontFormControl', 'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', 'PhabricatorFlagsApplication' => 'PhabricatorApplication', 'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorFundApplication' => 'PhabricatorApplication', 'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorGarbageCollector' => 'Phobject', 'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorGestureUIExample' => 'PhabricatorUIExample', 'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream', 'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorGlobalLock' => 'PhutilLock', 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorHandleList' => array( 'Phobject', 'Iterator', 'ArrayAccess', 'Countable', ), 'PhabricatorHandleObjectSelectorDataView' => 'Phobject', 'PhabricatorHandlePool' => 'Phobject', 'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase', 'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorHarbormasterApplication' => 'PhabricatorApplication', 'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorHash' => 'Phobject', 'PhabricatorHashTestCase' => 'PhabricatorTestCase', 'PhabricatorHelpApplication' => 'PhabricatorApplication', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController', 'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 'PhabricatorHeraldApplication' => 'PhabricatorApplication', 'PhabricatorHomeApplication' => 'PhabricatorApplication', 'PhabricatorHomeController' => 'PhabricatorController', 'PhabricatorHomeMainController' => 'PhabricatorHomeController', 'PhabricatorHomePreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorHomeQuickCreateController' => 'PhabricatorHomeController', 'PhabricatorHovercardUIExample' => 'PhabricatorUIExample', 'PhabricatorHovercardView' => 'AphrontView', 'PhabricatorHunksManagementMigrateWorkflow' => 'PhabricatorHunksManagementWorkflow', 'PhabricatorHunksManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorIRCProtocolAdapter' => 'PhabricatorProtocolAdapter', 'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageTransformer' => 'Phobject', 'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', 'PhabricatorInlineCommentController' => 'PhabricatorController', 'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface', 'PhabricatorInlineCommentPreviewController' => 'PhabricatorController', 'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow', 'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorJumpNavHandler' => 'Phobject', 'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache', 'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorLegalpadApplication' => 'PhabricatorApplication', 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType', 'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase', 'PhabricatorLipsumArtist' => 'Phobject', 'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow', 'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLiskSerializer' => 'Phobject', 'PhabricatorListFilterUIExample' => 'PhabricatorUIExample', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorLocaleScopeGuard' => 'Phobject', 'PhabricatorLocaleScopeGuardTestCase' => 'PhabricatorTestCase', 'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction', 'PhabricatorLogoutController' => 'PhabricatorAuthController', 'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorMacroApplication' => 'PhabricatorApplication', 'PhabricatorMacroAudioController' => 'PhabricatorMacroController', 'PhabricatorMacroCommentController' => 'PhabricatorMacroController', 'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMacroController' => 'PhabricatorController', 'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorMacroDisableController' => 'PhabricatorMacroController', 'PhabricatorMacroEditController' => 'PhabricatorMacroController', 'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorMacroListController' => 'PhabricatorMacroController', 'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType', 'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver', 'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability', 'PhabricatorMacroMemeController' => 'PhabricatorMacroController', 'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController', 'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorMacroTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailImplementationAdapter' => 'Phobject', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', + 'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorMailReceiver' => 'Phobject', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMailTarget' => 'Phobject', 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMainMenuSearchView' => 'AphrontView', 'PhabricatorMainMenuView' => 'AphrontView', 'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorManiphestApplication' => 'PhabricatorApplication', 'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 'PhabricatorMarkupEngine' => 'Phobject', 'PhabricatorMarkupOneOff' => array( 'Phobject', 'PhabricatorMarkupInterface', ), 'PhabricatorMarkupPreviewController' => 'PhabricatorController', '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', 'PhabricatorMetaMTAAttachment' => 'Phobject', 'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTAEmailBodyParser' => 'Phobject', 'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase', + 'PhabricatorMetaMTAEmailHeraldAction' => 'HeraldAction', + 'PhabricatorMetaMTAEmailOthersHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction', + 'PhabricatorMetaMTAEmailSelfHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction', 'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction', 'PhabricatorMetaMTAMail' => array( 'PhabricatorMetaMTADAO', 'PhabricatorPolicyInterface', ), 'PhabricatorMetaMTAMailBody' => 'Phobject', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType', 'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType', 'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorMetaMTAMailSection' => 'Phobject', 'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailViewController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAPermanentFailureException' => 'Exception', 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception', 'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', 'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample', 'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorMultimeterApplication' => 'PhabricatorApplication', 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLSearchEngine' => 'PhabricatorSearchEngine', 'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorNamedQuery' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorNotificationBuilder' => 'Phobject', 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 'PhabricatorNotificationClient' => 'Phobject', 'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorNotificationController' => 'PhabricatorController', 'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController', 'PhabricatorNotificationListController' => 'PhabricatorNotificationController', 'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController', 'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', 'PhabricatorNotificationStatusView' => 'AphrontTagView', 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationUIExample' => 'PhabricatorUIExample', 'PhabricatorNotificationsApplication' => 'PhabricatorApplication', 'PhabricatorNuanceApplication' => 'PhabricatorApplication', 'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider', 'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider', 'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorOAuthClientAuthorization' => array( 'PhabricatorOAuthServerDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthClientDeleteController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController', 'PhabricatorOAuthResponse' => 'AphrontResponse', 'PhabricatorOAuthServer' => 'Phobject', 'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerApplication' => 'PhabricatorApplication', 'PhabricatorOAuthServerAuthController' => 'PhabricatorAuthController', 'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorOAuthServerClient' => array( 'PhabricatorOAuthServerDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType', 'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType', 'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorOAuthServerController' => 'PhabricatorController', 'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability', 'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO', 'PhabricatorOAuthServerScope' => 'Phobject', 'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase', 'PhabricatorOAuthServerTestController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController', 'PhabricatorObjectHandle' => array( 'Phobject', 'PhabricatorPolicyInterface', ), 'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType', 'PhabricatorObjectHasContributorEdgeType' => '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', 'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorObjectSelectorDialog' => 'Phobject', 'PhabricatorObjectUsesCredentialsEdgeType' => 'PhabricatorEdgeType', 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorOwnerPathQuery' => 'Phobject', 'PhabricatorOwnersApplication' => 'PhabricatorApplication', 'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorOwnersController' => 'PhabricatorController', 'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO', 'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController', 'PhabricatorOwnersEditController' => 'PhabricatorOwnersController', 'PhabricatorOwnersListController' => 'PhabricatorOwnersController', 'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO', 'PhabricatorOwnersPackage' => array( 'PhabricatorOwnersDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType', 'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase', 'PhabricatorOwnersPackageTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO', 'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController', 'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', 'PhabricatorPHIDType' => 'Phobject', 'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase', 'PhabricatorPHPASTApplication' => 'PhabricatorApplication', 'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorPHPMailerConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPagedFormUIExample' => 'PhabricatorUIExample', 'PhabricatorPagerUIExample' => 'PhabricatorUIExample', 'PhabricatorPassphraseApplication' => 'PhabricatorApplication', 'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorPasswordHasher' => 'Phobject', 'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorPasswordHasherUnavailableException' => 'Exception', 'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorPaste' => array( 'PhabricatorPasteDAO', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorMentionableInterface', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorPasteApplication' => 'PhabricatorApplication', 'PhabricatorPasteCommentController' => 'PhabricatorPasteController', 'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPasteController' => 'PhabricatorController', 'PhabricatorPasteDAO' => 'PhabricatorLiskDAO', 'PhabricatorPasteEditController' => 'PhabricatorPasteController', 'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType', 'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPasteTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPasteViewController' => 'PhabricatorPasteController', 'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleApplication' => 'PhabricatorApplication', 'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController', 'PhabricatorPeopleCalendarController' => 'PhabricatorPeopleController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController', 'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController', 'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController', 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleHovercardEventListener' => 'PhabricatorEventListener', 'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController', 'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController', 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', 'PhabricatorPeopleNewController' => 'PhabricatorPeopleController', 'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleController', 'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController', 'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController', 'PhabricatorPersonaAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorPhameApplication' => 'PhabricatorApplication', 'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhluxApplication' => 'PhabricatorApplication', 'PhabricatorPholioApplication' => 'PhabricatorApplication', 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', 'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPhragmentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhrictionApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhurlApplication' => 'PhabricatorApplication', 'PhabricatorPhurlController' => 'PhabricatorController', 'PhabricatorPhurlDAO' => 'PhabricatorLiskDAO', 'PhabricatorPhurlSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPhurlURL' => array( 'PhabricatorPhurlDAO', 'PhabricatorPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorMentionableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorPhurlURLEditController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPhurlURLListController' => 'PhabricatorPhurlController', 'PhabricatorPhurlURLPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPhurlURLQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPhurlURLSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPhurlURLTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorPhurlURLTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorPhurlURLTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController', 'PhabricatorPlatformSite' => 'PhabricatorSite', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( 'PhabricatorPolicyDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorPolicyApplication' => 'PhabricatorApplication', 'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability', 'PhabricatorPolicyCapability' => 'Phobject', 'PhabricatorPolicyCapabilityTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPolicyConstants' => 'Phobject', 'PhabricatorPolicyController' => 'PhabricatorController', 'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO', 'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyEditController' => 'PhabricatorPolicyController', 'PhabricatorPolicyException' => 'Exception', 'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController', 'PhabricatorPolicyFilter' => 'Phobject', 'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface', 'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', 'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPolicyRule' => 'Phobject', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => array( 'Phobject', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', ), 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 'PhabricatorPonderApplication' => 'PhabricatorApplication', 'PhabricatorProject' => array( 'PhabricatorProjectDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', ), + 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumn' => array( 'PhabricatorProjectDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectColumnPosition' => array( 'PhabricatorProjectDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( 'PhabricatorProjectStandardCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCustomField' => 'PhabricatorCustomField', 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectEditDetailsController' => 'PhabricatorProjectController', 'PhabricatorProjectEditIconController' => 'PhabricatorProjectController', 'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController', 'PhabricatorProjectEditorTestCase' => 'PhabricatorTestCase', 'PhabricatorProjectFeedController' => 'PhabricatorProjectController', + 'PhabricatorProjectHeraldAction' => 'HeraldAction', 'PhabricatorProjectIcon' => 'Phobject', 'PhabricatorProjectListController' => 'PhabricatorProjectController', 'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectRemoveHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorProjectSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorProjectSlug' => 'PhabricatorProjectDAO', 'PhabricatorProjectStandardCustomField' => array( 'PhabricatorProjectCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorProjectStatus' => 'Phobject', 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 'PhabricatorProjectViewController' => 'PhabricatorProjectController', 'PhabricatorProjectWatchController' => 'PhabricatorProjectController', 'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorProtocolAdapter' => 'Phobject', 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorQuery' => 'Phobject', 'PhabricatorQueryConstraint' => 'Phobject', 'PhabricatorQueryOrderItem' => 'Phobject', 'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase', 'PhabricatorQueryOrderVector' => array( 'Phobject', 'Iterator', ), 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRecipientHasBadgeEdgeType' => 'PhabricatorEdgeType', 'PhabricatorRedirectController' => 'PhabricatorController', 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController', 'PhabricatorRegistrationProfile' => 'Phobject', 'PhabricatorReleephApplication' => 'PhabricatorApplication', 'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl', 'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule', 'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule', 'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupGraphvizBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample', 'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorRepository' => array( 'PhabricatorRepositoryDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorMarkupInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', 'PhabricatorSpacesInterface', ), 'PhabricatorRepositoryAuditRequest' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryCommit' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorSubscribableInterface', 'PhabricatorMentionableInterface', 'HarbormasterBuildableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker', 'PhabricatorRepositoryCommitRef' => 'Phobject', 'PhabricatorRepositoryCommitSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO', 'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorRepositoryEngine' => 'Phobject', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryGraphCache' => 'Phobject', 'PhabricatorRepositoryGraphStream' => 'Phobject', 'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementEditWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMovePathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryMirror' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryMirrorPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryMirrorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryParsedChange' => 'Phobject', 'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon', 'PhabricatorRepositoryPushEvent' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPushLog' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker', 'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryRefCursor' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorRepositoryType' => 'Phobject', 'PhabricatorRepositoryURINormalizer' => 'Phobject', 'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryVersion' => 'Phobject', 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorSMS' => 'PhabricatorSMSDAO', 'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSMSDAO' => 'PhabricatorLiskDAO', 'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker', 'PhabricatorSMSImplementationAdapter' => 'Phobject', 'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter', 'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter', 'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow', 'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow', 'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow', 'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker', 'PhabricatorSMSWorker' => 'PhabricatorWorker', 'PhabricatorSQLPatchList' => 'Phobject', 'PhabricatorSSHKeyGenerator' => 'Phobject', 'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSSHLog' => 'Phobject', 'PhabricatorSSHPassthruCommand' => 'Phobject', 'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSavedQuery' => array( 'PhabricatorSearchDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction', 'PhabricatorScopedEnv' => 'Phobject', 'PhabricatorSearchAbstractDocument' => 'Phobject', 'PhabricatorSearchApplication' => 'PhabricatorApplication', 'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField', 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField', 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchDateControlField' => 'PhabricatorSearchField', 'PhabricatorSearchDateField' => 'PhabricatorSearchField', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentFieldType' => 'Phobject', 'PhabricatorSearchDocumentIndexer' => 'Phobject', 'PhabricatorSearchDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchEngine' => 'Phobject', 'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorSearchField' => 'Phobject', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchIndexer' => 'Phobject', 'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchOrderField' => 'PhabricatorSearchField', 'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSearchRelationship' => 'Phobject', 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchSelectField' => 'PhabricatorSearchField', 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchTextField' => 'PhabricatorSearchField', 'PhabricatorSearchThreeStateField' => 'PhabricatorSearchField', 'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField', 'PhabricatorSearchWorker' => 'PhabricatorWorker', 'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction', 'PhabricatorSettingsAdjustController' => 'PhabricatorController', 'PhabricatorSettingsApplication' => 'PhabricatorApplication', 'PhabricatorSettingsMainController' => 'PhabricatorController', 'PhabricatorSettingsPanel' => 'Phobject', 'PhabricatorSetupCheck' => 'Phobject', 'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase', 'PhabricatorSetupIssue' => 'Phobject', 'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', 'PhabricatorSetupIssueView' => 'AphrontView', 'PhabricatorSite' => 'AphrontSite', 'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', 'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteController' => 'PhabricatorController', 'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO', 'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability', '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', + 'PhabricatorSlowvoteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSlowvoteTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController', 'PhabricatorSlug' => 'Phobject', 'PhabricatorSlugTestCase' => 'PhabricatorTestCase', 'PhabricatorSortTableUIExample' => 'PhabricatorUIExample', 'PhabricatorSourceCodeView' => 'AphrontView', 'PhabricatorSpacesApplication' => 'PhabricatorApplication', 'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController', 'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'PhabricatorSpacesController' => 'PhabricatorController', 'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO', 'PhabricatorSpacesEditController' => 'PhabricatorSpacesController', 'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface', 'PhabricatorSpacesListController' => 'PhabricatorSpacesController', 'PhabricatorSpacesNamespace' => array( 'PhabricatorSpacesDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorSpacesNamespaceDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorSpacesNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorSpacesNamespacePHIDType' => 'PhabricatorPHIDType', 'PhabricatorSpacesNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorSpacesNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSpacesNamespaceTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorSpacesNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorSpacesNoAccessController' => 'PhabricatorSpacesController', 'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSpacesSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSpacesTestCase' => 'PhabricatorTestCase', 'PhabricatorSpacesViewController' => 'PhabricatorSpacesController', 'PhabricatorStandardCustomField' => 'PhabricatorCustomField', 'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldPHIDs', 'PhabricatorStandardPageView' => 'PhabricatorBarePageView', 'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorStatusController' => 'PhabricatorController', 'PhabricatorStatusUIExample' => 'PhabricatorUIExample', 'PhabricatorStorageFixtureScopeGuard' => 'Phobject', 'PhabricatorStorageManagementAPI' => 'Phobject', 'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorStoragePatch' => 'Phobject', 'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorStreamingProtocolAdapter' => 'PhabricatorProtocolAdapter', 'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock', + 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', + 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', + 'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction', 'PhabricatorSubscriptionsListController' => 'PhabricatorController', + 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', + 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController', 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorSupportApplication' => 'PhabricatorApplication', 'PhabricatorSyntaxHighlighter' => 'Phobject', 'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSystemAction' => 'Phobject', 'PhabricatorSystemActionEngine' => 'Phobject', 'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO', 'PhabricatorSystemActionRateLimitException' => 'Exception', 'PhabricatorSystemApplication' => 'PhabricatorApplication', 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorSystemSelectEncodingController' => 'PhabricatorController', 'PhabricatorSystemSelectHighlightController' => 'PhabricatorController', 'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor', 'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 'PhabricatorTestApplication' => 'PhabricatorApplication', 'PhabricatorTestCase' => 'PhutilTestCase', 'PhabricatorTestController' => 'PhabricatorController', 'PhabricatorTestDataGenerator' => 'Phobject', 'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType', 'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorTestWorker' => 'PhabricatorWorker', 'PhabricatorTime' => 'Phobject', 'PhabricatorTimeGuard' => 'Phobject', 'PhabricatorTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorToken' => array( 'PhabricatorTokenDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorTokenController' => 'PhabricatorController', 'PhabricatorTokenCount' => 'PhabricatorTokenDAO', 'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorTokenDAO' => 'PhabricatorLiskDAO', 'PhabricatorTokenGiveController' => 'PhabricatorTokenController', 'PhabricatorTokenGiven' => array( 'PhabricatorTokenDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorTokenGivenController' => 'PhabricatorTokenController', 'PhabricatorTokenGivenEditor' => 'PhabricatorEditor', 'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory', 'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenLeaderController' => 'PhabricatorTokenController', 'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType', 'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener', 'PhabricatorTokensApplication' => 'PhabricatorApplication', 'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorTooltipUIExample' => 'PhabricatorUIExample', 'PhabricatorTransactions' => 'Phobject', 'PhabricatorTransactionsApplication' => 'PhabricatorApplication', 'PhabricatorTransformedFile' => 'PhabricatorFileDAO', 'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorTriggerAction' => 'Phobject', 'PhabricatorTriggerClock' => 'Phobject', 'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase', 'PhabricatorTriggerDaemon' => 'PhabricatorDaemon', 'PhabricatorTrivialTestCase' => 'PhabricatorTestCase', 'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider', - 'PhabricatorTwoColumnUIExample' => 'PhabricatorUIExample', 'PhabricatorTypeaheadApplication' => 'PhabricatorApplication', 'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadDatasource' => 'Phobject', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadInvalidTokenException' => 'Exception', 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadResult' => 'Phobject', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadTokenView' => 'AphrontTagView', 'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUIExample' => 'Phobject', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', 'PhabricatorUSEnglishTranslation' => 'PhutilTranslation', 'PhabricatorUnitsTestCase' => 'PhabricatorTestCase', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorUser' => array( 'PhabricatorUserDAO', 'PhutilPerson', 'PhabricatorPolicyInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSSHPublicKeyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUserConfiguredCustomField' => array( 'PhabricatorUserCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorUserCustomField' => 'PhabricatorCustomField', 'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase', 'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase', 'PhabricatorUserLog' => array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorUserLogView' => 'AphrontView', 'PhabricatorUserPreferences' => 'PhabricatorUserDAO', 'PhabricatorUserProfile' => 'PhabricatorUserDAO', 'PhabricatorUserProfileEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField', 'PhabricatorUserRolesField' => 'PhabricatorUserCustomField', 'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorUserSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorUserSinceField' => 'PhabricatorUserCustomField', 'PhabricatorUserStatusField' => 'PhabricatorUserCustomField', 'PhabricatorUserTestCase' => 'PhabricatorTestCase', 'PhabricatorUserTitleField' => 'PhabricatorUserCustomField', 'PhabricatorUserTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorVCSResponse' => 'AphrontResponse', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorWorker' => 'Phobject', 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorQuery', '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', 'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery', 'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow', 'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorWorkerPermanentFailureException' => 'Exception', 'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController', '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', 'PhabricatorXHPASTViewController' => 'PhabricatorController', 'PhabricatorXHPASTViewDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewParseTree' => 'PhabricatorXHPASTViewDAO', 'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHProfApplication' => 'PhabricatorApplication', 'PhabricatorXHProfController' => 'PhabricatorController', 'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController', 'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileView' => 'AphrontView', 'PhabricatorXHProfSample' => 'PhabricatorXHProfDAO', 'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController', 'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule', 'PhameBasicBlogSkin' => 'PhameBlogSkin', 'PhameBasicTemplateBlogSkin' => 'PhameBasicBlogSkin', 'PhameBlog' => array( 'PhameDAO', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhameBlogDeleteController' => 'PhameController', 'PhameBlogEditController' => 'PhameController', 'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor', 'PhameBlogFeedController' => 'PhameController', 'PhameBlogListController' => 'PhameController', 'PhameBlogLiveController' => 'PhameController', 'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhameBlogResourceSite' => 'PhameSite', 'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhameBlogSite' => 'PhameSite', 'PhameBlogSkin' => 'PhabricatorController', 'PhameBlogTransaction' => 'PhabricatorApplicationTransaction', 'PhameBlogViewController' => 'PhameController', 'PhameCelerityResources' => 'CelerityResources', 'PhameConduitAPIMethod' => 'ConduitAPIMethod', 'PhameController' => 'PhabricatorController', 'PhameCreatePostConduitAPIMethod' => 'PhameConduitAPIMethod', 'PhameDAO' => 'PhabricatorLiskDAO', 'PhamePost' => array( 'PhameDAO', 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', ), 'PhamePostDeleteController' => 'PhameController', 'PhamePostEditController' => 'PhameController', 'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor', 'PhamePostFramedController' => 'PhameController', 'PhamePostListController' => 'PhameController', 'PhamePostNewController' => 'PhameController', 'PhamePostNotLiveController' => 'PhameController', 'PhamePostPreviewController' => 'PhameController', 'PhamePostPublishController' => 'PhameController', 'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhamePostSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhamePostTransaction' => 'PhabricatorApplicationTransaction', 'PhamePostTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhamePostUnpublishController' => 'PhameController', 'PhamePostView' => 'AphrontView', 'PhamePostViewController' => 'PhameController', 'PhameQueryConduitAPIMethod' => 'PhameConduitAPIMethod', 'PhameQueryPostsConduitAPIMethod' => 'PhameConduitAPIMethod', 'PhameResourceController' => 'CelerityResourceController', 'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhameSite' => 'PhabricatorSite', 'PhameSkinSpecification' => 'Phobject', 'PhluxController' => 'PhabricatorController', 'PhluxDAO' => 'PhabricatorLiskDAO', 'PhluxEditController' => 'PhluxController', 'PhluxListController' => 'PhluxController', 'PhluxTransaction' => 'PhabricatorApplicationTransaction', 'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhluxVariable' => array( 'PhluxDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', ), 'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor', 'PhluxVariablePHIDType' => 'PhabricatorPHIDType', 'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhluxViewController' => 'PhluxController', 'PholioActionMenuEventListener' => 'PhabricatorEventListener', 'PholioController' => 'PhabricatorController', 'PholioDAO' => 'PhabricatorLiskDAO', 'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PholioImage' => array( 'PholioDAO', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', ), 'PholioImagePHIDType' => 'PhabricatorPHIDType', 'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PholioImageUploadController' => 'PholioController', 'PholioInlineController' => 'PholioController', 'PholioInlineListController' => 'PholioController', 'PholioMock' => array( 'PholioDAO', 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', 'PhabricatorMentionableInterface', ), 'PholioMockAuthorHeraldField' => 'PholioMockHeraldField', 'PholioMockCommentController' => 'PholioController', 'PholioMockDescriptionHeraldField' => 'PholioMockHeraldField', 'PholioMockEditController' => 'PholioController', 'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor', 'PholioMockEmbedView' => 'AphrontView', 'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType', 'PholioMockHeraldField' => 'HeraldField', 'PholioMockHeraldFieldGroup' => 'HeraldFieldGroup', 'PholioMockImagesView' => 'AphrontView', 'PholioMockListController' => 'PholioController', 'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver', 'PholioMockNameHeraldField' => 'PholioMockHeraldField', 'PholioMockPHIDType' => 'PhabricatorPHIDType', 'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PholioMockThumbGridView' => 'AphrontView', 'PholioMockViewController' => 'PholioController', 'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PholioSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PholioTransaction' => 'PhabricatorApplicationTransaction', 'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PholioTransactionView' => 'PhabricatorApplicationTransactionView', 'PholioUploadedImageView' => 'AphrontView', 'PhortuneAccount' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneAccountListController' => 'PhortuneController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountViewController' => 'PhortuneController', 'PhortuneAdHocCart' => 'PhortuneCartImplementation', 'PhortuneAdHocProduct' => 'PhortuneProductImplementation', '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', ), 'PhortuneChargeListController' => 'PhortuneController', 'PhortuneChargePHIDType' => 'PhabricatorPHIDType', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneChargeTableView' => 'AphrontView', 'PhortuneConstants' => 'Phobject', 'PhortuneController' => 'PhabricatorController', 'PhortuneCreditCardForm' => 'Phobject', 'PhortuneCurrency' => 'Phobject', 'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer', 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchant' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), 'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability', 'PhortuneMerchantController' => 'PhortuneController', 'PhortuneMerchantEditController' => 'PhortuneMerchantController', 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController', 'PhortuneMerchantListController' => 'PhortuneMerchantController', 'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType', 'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneMerchantTransaction' => 'PhabricatorApplicationTransaction', 'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneMerchantViewController' => 'PhortuneMerchantController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneNotImplementedException' => 'Exception', 'PhortuneOrderTableView' => 'AphrontView', 'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider', 'PhortunePaymentMethod' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), 'PhortunePaymentMethodCreateController' => 'PhortuneController', 'PhortunePaymentMethodDisableController' => 'PhortuneController', 'PhortunePaymentMethodEditController' => 'PhortuneController', 'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentProvider' => 'Phobject', 'PhortunePaymentProviderConfig' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), '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', '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', 'PhabricatorMarkupInterface', ), 'PhrictionController' => 'PhabricatorController', 'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionDAO' => 'PhabricatorLiskDAO', 'PhrictionDeleteController' => 'PhrictionController', 'PhrictionDiffController' => 'PhrictionController', 'PhrictionDocument' => array( 'PhrictionDAO', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', ), 'PhrictionDocumentAuthorHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentContentHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentController' => 'PhrictionController', 'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter', 'PhrictionDocumentHeraldField' => 'HeraldField', 'PhrictionDocumentHeraldFieldGroup' => 'HeraldFieldGroup', 'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType', 'PhrictionDocumentPathHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhrictionDocumentStatus' => 'PhrictionConstants', 'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField', 'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionEditController' => 'PhrictionController', 'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionHistoryController' => 'PhrictionController', 'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod', 'PhrictionListController' => 'PhrictionController', 'PhrictionMoveController' => 'PhrictionController', 'PhrictionNewController' => 'PhrictionController', 'PhrictionRemarkupRule' => 'PhutilRemarkupRule', 'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrictionSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhrictionTransaction' => 'PhabricatorApplicationTransaction', 'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType', 'PonderAddAnswerView' => 'AphrontView', 'PonderAnswer' => array( 'PonderDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMarkupInterface', 'PonderVotableInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorDestructibleInterface', ), 'PonderAnswerCommentController' => 'PonderController', 'PonderAnswerEditController' => 'PonderController', 'PonderAnswerEditor' => 'PonderEditor', 'PonderAnswerHasVotingUserEdgeType' => 'PhabricatorEdgeType', 'PonderAnswerHistoryController' => 'PonderController', 'PonderAnswerPHIDType' => 'PhabricatorPHIDType', 'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PonderAnswerSaveController' => 'PonderController', 'PonderAnswerTransaction' => 'PhabricatorApplicationTransaction', 'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PonderConstants' => 'Phobject', 'PonderController' => 'PhabricatorController', 'PonderDAO' => 'PhabricatorLiskDAO', 'PonderEditor' => 'PhabricatorApplicationTransactionEditor', 'PonderQuestion' => array( 'PonderDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMarkupInterface', 'PonderVotableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorSpacesInterface', ), 'PonderQuestionCommentController' => 'PonderController', + 'PonderQuestionDefaultEditCapability' => 'PhabricatorPolicyCapability', + 'PonderQuestionDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PonderQuestionEditController' => 'PonderController', 'PonderQuestionEditor' => 'PonderEditor', 'PonderQuestionHasVotingUserEdgeType' => 'PhabricatorEdgeType', 'PonderQuestionHistoryController' => 'PonderController', 'PonderQuestionListController' => 'PonderController', 'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver', 'PonderQuestionPHIDType' => 'PhabricatorPHIDType', 'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PonderQuestionStatus' => 'PonderConstants', 'PonderQuestionStatusController' => 'PonderController', 'PonderQuestionTransaction' => 'PhabricatorApplicationTransaction', 'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PonderQuestionViewController' => 'PonderController', 'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PonderSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'PonderTransactionFeedStory' => 'PhabricatorApplicationTransactionFeedStory', 'PonderVotableView' => 'AphrontView', 'PonderVote' => 'PonderConstants', 'PonderVoteEditor' => 'PhabricatorEditor', 'PonderVoteSaveController' => 'PonderController', 'PonderVotingUserHasAnswerEdgeType' => 'PhabricatorEdgeType', 'PonderVotingUserHasQuestionEdgeType' => 'PhabricatorEdgeType', 'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand', 'ProjectBoardTaskCard' => 'Phobject', 'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability', 'ProjectConduitAPIMethod' => 'ConduitAPIMethod', 'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', 'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', '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', 'RepositoryCreateConduitAPIMethod' => 'RepositoryConduitAPIMethod', 'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod', 'ShellLogView' => 'AphrontView', 'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod', 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod', 'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'SubscriptionListDialogBuilder' => 'Phobject', 'SubscriptionListStringBuilder' => 'Phobject', 'TokenConduitAPIMethod' => 'ConduitAPIMethod', 'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod', 'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod', 'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod', 'UserConduitAPIMethod' => 'ConduitAPIMethod', 'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod', 'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod', 'UserFindConduitAPIMethod' => 'UserConduitAPIMethod', 'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod', 'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod', ), )); diff --git a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php index 92c9beb933..b4bb950806 100644 --- a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php +++ b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php @@ -1,93 +1,92 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); if (!$request->isFormPost()) { return new Aphront403Response(); } $commit_phid = $request->getStr('commit'); $commit = id(new DiffusionCommitQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($commit_phid)) ->needAuditRequests(true) ->executeOne(); if (!$commit) { return new Aphront404Response(); } $xactions = array(); // make sure we only add auditors or ccs if the action matches $action = $request->getStr('action'); switch ($action) { case PhabricatorAuditActionConstants::ADD_AUDITORS: $auditors = $request->getArr('auditors'); $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) ->setNewValue(array_fuse($auditors)); break; case PhabricatorAuditActionConstants::ADD_CCS: $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue( array( '+' => $request->getArr('ccs'), )); break; case PhabricatorAuditActionConstants::COMMENT: // We'll deal with this below. break; default: $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ACTION) ->setNewValue($action); break; } $content = $request->getStr('content'); if (strlen($content)) { $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorAuditTransactionComment()) ->setCommitPHID($commit->getPHID()) ->setContent($content)); } $inlines = PhabricatorAuditInlineComment::loadDraftComments( - $user, + $viewer, $commit->getPHID()); foreach ($inlines as $inline) { $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::INLINE) ->attachComment($inline->getTransactionComment()); } id(new PhabricatorAuditEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->applyTransactions($commit, $xactions); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', - $user->getPHID(), + $viewer->getPHID(), 'diffusion-audit-'.$commit->getID()); if ($draft) { $draft->delete(); } $monogram = $commit->getRepository()->getMonogram(); $identifier = $commit->getCommitIdentifier(); $uri = '/'.$monogram.$identifier; return id(new AphrontRedirectResponse())->setURI($uri); } } diff --git a/src/applications/audit/controller/PhabricatorAuditPreviewController.php b/src/applications/audit/controller/PhabricatorAuditPreviewController.php index f27b55ae34..2b0212370c 100644 --- a/src/applications/audit/controller/PhabricatorAuditPreviewController.php +++ b/src/applications/audit/controller/PhabricatorAuditPreviewController.php @@ -1,88 +1,82 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $commit = id(new PhabricatorRepositoryCommit())->load($this->id); + $commit = id(new PhabricatorRepositoryCommit())->load($id); if (!$commit) { return new Aphront404Response(); } $xactions = array(); $action = $request->getStr('action'); if ($action != PhabricatorAuditActionConstants::COMMENT) { $action_xaction = id(new PhabricatorAuditTransaction()) - ->setAuthorPHID($user->getPHID()) + ->setAuthorPHID($viewer->getPHID()) ->setObjectPHID($commit->getPHID()) ->setTransactionType(PhabricatorAuditActionConstants::ACTION) ->setNewValue($action); $auditors = $request->getStrList('auditors'); if ($action == PhabricatorAuditActionConstants::ADD_AUDITORS && $auditors) { $action_xaction->setTransactionType($action); $action_xaction->setNewValue(array_fuse($auditors)); } $ccs = $request->getStrList('ccs'); if ($action == PhabricatorAuditActionConstants::ADD_CCS && $ccs) { $action_xaction->setTransactionType( PhabricatorTransactions::TYPE_SUBSCRIBERS); // NOTE: This doesn't get processed before use, so just provide fake // values. $action_xaction->setOldValue(array()); $action_xaction->setNewValue($ccs); } $xactions[] = $action_xaction; } $content = $request->getStr('content'); if (strlen($content)) { $xactions[] = id(new PhabricatorAuditTransaction()) - ->setAuthorPHID($user->getPHID()) + ->setAuthorPHID($viewer->getPHID()) ->setObjectPHID($commit->getPHID()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorAuditTransactionComment()) ->setContent($content)); } $phids = array(); foreach ($xactions as $xaction) { $phids[] = $xaction->getRequiredHandlePHIDs(); } $phids = array_mergev($phids); $handles = $this->loadViewerHandles($phids); foreach ($xactions as $xaction) { $xaction->setHandles($handles); } $view = id(new PhabricatorAuditTransactionView()) ->setIsPreview(true) - ->setUser($user) + ->setUser($viewer) ->setObjectPHID($commit->getPHID()) ->setTransactions($xactions); id(new PhabricatorDraft()) - ->setAuthorPHID($user->getPHID()) - ->setDraftKey('diffusion-audit-'.$this->id) + ->setAuthorPHID($viewer->getPHID()) + ->setDraftKey('diffusion-audit-'.$id) ->setDraft($content) ->replaceOrDelete(); return id(new AphrontAjaxResponse())->setContent(hsprintf('%s', $view)); } } diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 95f8ee8311..7c0f3d2e34 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -1,997 +1,968 @@ auditReasonMap[$phid])) { $this->auditReasonMap[$phid] = array(); } $this->auditReasonMap[$phid][] = $reason; return $this; } private function getAuditReasons($phid) { if (isset($this->auditReasonMap[$phid])) { return $this->auditReasonMap[$phid]; } if ($this->getIsHeraldEditor()) { $name = 'herald'; } else { $name = $this->getActor()->getUsername(); } return array(pht('Added by %s.', $name)); } public function setRawPatch($patch) { $this->rawPatch = $patch; return $this; } public function getRawPatch() { return $this->rawPatch; } public function getEditorApplicationClass() { return 'PhabricatorAuditApplication'; } public function getEditorObjectsDescription() { return pht('Audits'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_INLINESTATE; $types[] = PhabricatorAuditTransaction::TYPE_COMMIT; // TODO: These will get modernized eventually, but that can happen one // at a time later on. $types[] = PhabricatorAuditActionConstants::ACTION; $types[] = PhabricatorAuditActionConstants::INLINE; $types[] = PhabricatorAuditActionConstants::ADD_AUDITORS; return $types; } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: return $xaction->hasComment(); } return parent::transactionHasEffect($object, $xaction); } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditTransaction::TYPE_COMMIT: return null; case PhabricatorAuditActionConstants::ADD_AUDITORS: // TODO: For now, just record the added PHIDs. Eventually, turn these // into real edge transactions, probably? return array(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditTransaction::TYPE_COMMIT: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditTransaction::TYPE_COMMIT: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditTransaction::TYPE_COMMIT: return; case PhabricatorAuditActionConstants::INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; case PhabricatorAuditActionConstants::ADD_AUDITORS: $new = $xaction->getNewValue(); if (!is_array($new)) { $new = array(); } $old = $xaction->getOldValue(); if (!is_array($old)) { $old = array(); } $add = array_diff_key($new, $old); $actor = $this->requireActor(); $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); foreach ($add as $phid) { if (isset($requests[$phid])) { continue; } if ($this->getIsHeraldEditor()) { $audit_requested = $xaction->getMetadataValue('auditStatus'); $audit_reason_map = $xaction->getMetadataValue('auditReasonMap'); $audit_reason = $audit_reason_map[$phid]; } else { $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED; $audit_reason = $this->getAuditReasons($phid); } - $requests[] = id (new PhabricatorRepositoryAuditRequest()) + $requests[] = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($phid) ->setAuditStatus($audit_requested) ->setAuditReasons($audit_reason) ->save(); } $object->attachAudits($requests); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $table = new PhabricatorAuditTransactionComment(); $conn_w = $table->establishConnection('w'); foreach ($xaction->getNewValue() as $phid => $state) { queryfx( $conn_w, 'UPDATE %T SET fixedState = %s WHERE phid = %s', $table->getTableName(), $state, $phid); } break; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load auditors explicitly; we may not have them if the caller was a // generic piece of infrastructure. $commit = id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needAuditRequests(true) ->executeOne(); if (!$commit) { throw new Exception( pht('Failed to load commit during transaction finalization!')); } $object->attachAudits($commit->getAudits()); $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $status_closed = PhabricatorAuditStatusConstants::CLOSED; $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; $status_accepted = PhabricatorAuditStatusConstants::ACCEPTED; $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID()) && ($actor_phid == $object->getAuthorPHID()); $import_status_flag = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD; break; case PhabricatorAuditActionConstants::ACTION: $new = $xaction->getNewValue(); switch ($new) { case PhabricatorAuditActionConstants::CLOSE: // "Close" means wipe out all the concerns. $requests = $object->getAudits(); foreach ($requests as $request) { if ($request->getAuditStatus() == $status_concerned) { $request ->setAuditStatus($status_closed) ->save(); } } break; case PhabricatorAuditActionConstants::RESIGN: $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); $actor_request = idx($requests, $actor_phid); // If the actor doesn't currently have a relationship to the // commit, add one explicitly. For example, this allows members // of a project to resign from a commit and have it drop out of // their queue. if (!$actor_request) { $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($actor_phid); $requests[] = $actor_request; $object->attachAudits($requests); } $actor_request ->setAuditStatus($status_resigned) ->save(); break; case PhabricatorAuditActionConstants::ACCEPT: case PhabricatorAuditActionConstants::CONCERN: if ($new == PhabricatorAuditActionConstants::ACCEPT) { $new_status = $status_accepted; } else { $new_status = $status_concerned; } $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); // Figure out which requests the actor has authority over: these // are user requests where they are the auditor, and packages // and projects they are a member of. if ($actor_is_author) { // When modifying your own commits, you act only on behalf of // yourself, not your packages/projects -- the idea being that // you can't accept your own commits. $authority_phids = array($actor_phid); } else { $authority_phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser( $this->requireActor()); } $authority = array_select_keys( $requests, $authority_phids); if (!$authority) { // If the actor has no authority over any existing requests, // create a new request for them. $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($actor_phid) ->setAuditStatus($new_status) ->save(); $requests[$actor_phid] = $actor_request; $object->attachAudits($requests); } else { // Otherwise, update the audit status of the existing requests. foreach ($authority as $request) { $request ->setAuditStatus($new_status) ->save(); } } break; } break; } } $requests = $object->getAudits(); $object->updateAuditStatus($requests); $object->save(); if ($import_status_flag) { $object->writeImportStatusFlag($import_status_flag); } // Collect auditor PHIDs for building mail. $this->auditorPHIDs = mpull($object->getAudits(), 'getAuditorPHID'); return $xactions; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = parent::expandTransaction($object, $xaction); switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $request = $this->createAuditRequestTransactionFromCommitMessage( $object); if ($request) { $xactions[] = $request; $this->setUnmentionablePHIDMap($request->getNewValue()); } break; default: break; } if (!$this->didExpandInlineState) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorAuditActionConstants::ACTION: $this->didExpandInlineState = true; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID() == $actor_phid); if (!$actor_is_author) { break; } $state_map = PhabricatorTransactions::getInlineStateMap(); $inlines = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($this->getActor()) ->withCommitPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)) ->execute(); if (!$inlines) { break; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); break; } } return $xactions; } private function createAuditRequestTransactionFromCommitMessage( PhabricatorRepositoryCommit $commit) { $data = $commit->getCommitData(); $message = $data->getCommitMessage(); $matches = null; if (!preg_match('/^Auditors?:\s*(.*)$/im', $message, $matches)) { return array(); } $phids = id(new PhabricatorObjectListQuery()) ->setViewer($this->getActor()) ->setAllowPartialResults(true) ->setAllowedTypes( array( PhabricatorPeopleUserPHIDType::TYPECONST, PhabricatorProjectProjectPHIDType::TYPECONST, )) ->setObjectList($matches[1]) ->execute(); if (!$phids) { return array(); } foreach ($phids as $phid) { $this->addAuditReason($phid, pht('Requested by Author')); } return id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) ->setNewValue(array_fuse($phids)); } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorAuditActionConstants::INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case PhabricatorAuditActionConstants::ACTION: $error = $this->validateAuditAction( $object, $type, $xaction, $xaction->getNewValue()); if ($error) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $error, $xaction); } break; } } return $errors; } private function validateAuditAction( PhabricatorLiskDAO $object, $type, PhabricatorAuditTransaction $xaction, $action) { $can_author_close_key = 'audit.can-author-close-audit'; $can_author_close = PhabricatorEnv::getEnvConfig($can_author_close_key); $actor_is_author = ($object->getAuthorPHID()) && ($object->getAuthorPHID() == $this->getActingAsPHID()); switch ($action) { case PhabricatorAuditActionConstants::CLOSE: if (!$actor_is_author) { return pht( 'You can not close this audit because you are not the author '. 'of the commit.'); } if (!$can_author_close) { return pht( 'You can not close this audit because "%s" is disabled in '. 'the Phabricator configuration.', $can_author_close_key); } break; } return null; } protected function supportsSearch() { return true; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, $blocks, PhutilMarkupEngine $engine) { // we are only really trying to find unmentionable phids here... // don't bother with this outside initial commit (i.e. create) // transaction $is_commit = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $is_commit = true; break; } } // "result" is always an array.... $result = array(); if (!$is_commit) { return $result; } $flat_blocks = array_mergev($blocks); $huge_block = implode("\n\n", $flat_blocks); $phid_map = array(); $phid_map[] = $this->getUnmentionablePHIDMap(); $monograms = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($huge_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($huge_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withNames($monograms) ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); $phid_map = array_mergev($phid_map); $this->setUnmentionablePHIDMap($phid_map); return $result; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { $reply_handler = new PhabricatorAuditReplyHandler(); $reply_handler->setMailReceiver($object); return $reply_handler; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // For backward compatibility, use this legacy thread ID. return 'diffusion-audit-'.$object->getPHID(); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $identifier = $object->getCommitIdentifier(); $repository = $object->getRepository(); $monogram = $repository->getMonogram(); $summary = $object->getSummary(); $name = $repository->formatCommitName($identifier); $subject = "{$name}: {$summary}"; $thread_topic = "Commit {$monogram}{$identifier}"; $template = id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addHeader('Thread-Topic', $thread_topic); $this->attachPatch( $template, $object); return $template; } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($object->getAuthorPHID()) { $phids[] = $object->getAuthorPHID(); } $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; foreach ($object->getAudits() as $audit) { if ($audit->getAuditStatus() != $status_resigned) { $phids[] = $audit->getAuditorPHID(); } } return $phids; } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $type_inline = PhabricatorAuditActionConstants::INLINE; $type_push = PhabricatorAuditTransaction::TYPE_COMMIT; $is_commit = false; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } if ($xaction->getTransactionType() == $type_push) { $is_commit = true; } } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } if ($is_commit) { $data = $object->getCommitData(); $body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles); $this->inlinePatch( $body, $object); } $data = $object->getCommitData(); $user_phids = array(); $author_phid = $object->getAuthorPHID(); if ($author_phid) { $user_phids[$author_phid][] = pht('Author'); } $committer_phid = $data->getCommitDetail('committerPHID'); if ($committer_phid && ($committer_phid != $author_phid)) { $user_phids[$committer_phid][] = pht('Committer'); } foreach ($this->auditorPHIDs as $auditor_phid) { $user_phids[$auditor_phid][] = pht('Auditor'); } // TODO: It would be nice to show pusher here too, but that information // is a little tricky to get at right now. if ($user_phids) { $handle_phids = array_keys($user_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($handle_phids) ->execute(); $user_info = array(); foreach ($user_phids as $phid => $roles) { $user_info[] = pht( '%s (%s)', $handles[$phid]->getName(), implode(', ', $roles)); } $body->addTextSection( pht('USERS'), implode("\n", $user_info)); } $monogram = $object->getRepository()->formatCommitName( $object->getCommitIdentifier()); $body->addLinkSection( pht('COMMIT'), PhabricatorEnv::getProductionURI('/'.$monogram)); return $body; } private function attachPatch( PhabricatorMetaMTAMail $template, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $attach_key = 'metamta.diffusion.attach-patches'; $attach_patches = PhabricatorEnv::getEnvConfig($attach_key); if (!$attach_patches) { return; } $repository = $commit->getRepository(); $encoding = $repository->getDetail('encoding', 'UTF-8'); $raw_patch = $this->getRawPatch(); $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $template->addAttachment( new PhabricatorMetaMTAAttachment( $raw_patch, $commit_name.'.patch', 'text/x-patch; charset='.$encoding)); } private function inlinePatch( PhabricatorMetaMTAMailBody $body, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $inline_key = 'metamta.diffusion.inline-patches'; $inline_patches = PhabricatorEnv::getEnvConfig($inline_key); if (!$inline_patches) { return; } $repository = $commit->getRepository(); $raw_patch = $this->getRawPatch(); $result = null; $len = substr_count($raw_patch, "\n"); if ($len <= $inline_patches) { // We send email as utf8, so we need to convert the text to utf8 if // we can. $encoding = $repository->getDetail('encoding', 'UTF-8'); if ($encoding) { $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding); } $result = phutil_utf8ize($raw_patch); } if ($result) { $result = "PATCH\n\n{$result}\n"; } $body->addRawSection($result); } private function renderInlineCommentsForMail( PhabricatorLiskDAO $object, array $inline_xactions) { $inlines = mpull($inline_xactions, 'getComment'); $block = array(); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($inlines, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path', 'id'); foreach ($inlines as $inline) { $path = idx($path_map, $inline->getPathID()); if ($path === null) { continue; } $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $block[] = "{$path}:{$range} {$content}"; } return implode("\n", $block); } public function getMailTagsMap() { return array( PhabricatorAuditTransaction::MAILTAG_COMMIT => pht('A commit is created.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN => pht('A commit has a concerned raised against it.'), PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT => pht('A commit is accepted.'), PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN => pht('A commit has an auditor resign.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE => pht('A commit is closed.'), PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS => pht('A commit has auditors added.'), PhabricatorAuditTransaction::MAILTAG_ADD_CCS => pht("A commit's subscribers change."), PhabricatorAuditTransaction::MAILTAG_PROJECTS => pht("A commit's projects change."), PhabricatorAuditTransaction::MAILTAG_COMMENT => pht('Someone comments on a commit.'), PhabricatorAuditTransaction::MAILTAG_OTHER => pht('Other commit activity not listed above occurs.'), ); } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $repository = $object->getRepository(); if (!$repository->shouldPublish()) { return false; } return true; default: break; } } return parent::shouldApplyHeraldRules($object, $xactions); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldCommitAdapter()) ->setCommit($object); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { - $xactions = array(); - - $audit_phids = $adapter->getAuditMap(); - foreach ($audit_phids as $phid => $rule_ids) { - foreach ($rule_ids as $rule_id) { - $this->addAuditReason( - $phid, - pht( - '%s Triggered Audit', - "H{$rule_id}")); - } - } - - if ($audit_phids) { - $xactions[] = id(new PhabricatorAuditTransaction()) - ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) - ->setNewValue(array_fuse(array_keys($audit_phids))) - ->setMetadataValue( - 'auditStatus', - PhabricatorAuditStatusConstants::AUDIT_REQUIRED) - ->setMetadataValue( - 'auditReasonMap', $this->auditReasonMap); - } - - HarbormasterBuildable::applyBuildPlans( - $object->getPHID(), - $object->getRepository()->getPHID(), - $adapter->getBuildPlans()); - $limit = self::MAX_FILES_SHOWN_IN_EMAIL; $files = $adapter->loadAffectedPaths(); sort($files); if (count($files) > $limit) { array_splice($files, $limit); $files[] = pht( '(This commit affected more than %d files. Only %d are shown here '. 'and additional ones are truncated.)', $limit, $limit); } $this->affectedFiles = implode("\n", $files); - return $xactions; + return array(); } private function isCommitMostlyImported(PhabricatorLiskDAO $object) { $has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE; $has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE; // Don't publish feed stories or email about events which occur during // import. In particular, this affects tasks being attached when they are // closed by "Fixes Txxxx" in a commit message. See T5851. $mask = ($has_message | $has_changes); return $object->isPartiallyImported($mask); } private function shouldPublishRepositoryActivity( PhabricatorLiskDAO $object, array $xactions) { // not every code path loads the repository so tread carefully // TODO: They should, and then we should simplify this. $repository = $object->getRepository($assert_attached = false); if ($repository != PhabricatorLiskDAO::ATTACHABLE) { if (!$repository->shouldPublish()) { return false; } } return $this->isCommitMostlyImported($object); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldEnableMentions( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function getCustomWorkerState() { return array( 'rawPatch' => $this->rawPatch, 'affectedFiles' => $this->affectedFiles, 'auditorPHIDs' => $this->auditorPHIDs, ); } protected function loadCustomWorkerState(array $state) { $this->rawPatch = idx($state, 'rawPatch'); $this->affectedFiles = idx($state, 'affectedFiles'); $this->auditorPHIDs = idx($state, 'auditorPHIDs'); return $this; } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { return id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needAuditRequests(true) ->needCommitData(true) ->executeOne(); } } diff --git a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php index 32c0101b8a..799a8e691e 100644 --- a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php @@ -1,87 +1,81 @@ getViewer(); + $accountkey = $request->getURIData('akey'); - public function willProcessRequest(array $data) { - $this->accountKey = idx($data, 'akey'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); + $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()) { $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) ->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()); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Confirm External Account Link'), )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php b/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php index c4b6b2ad43..4981845876 100644 --- a/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php +++ b/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php @@ -1,49 +1,48 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $panel_uri = '/settings/panel/sessions/'; $session = $viewer->getSession(); if ($session->getHighSecurityUntil() < time()) { return $this->newDialog() ->setTitle(pht('Normal Security Restored')) ->appendParagraph( pht('Your session is no longer in high security.')) ->addCancelButton($panel_uri, pht('Continue')); } if ($request->isFormPost()) { id(new PhabricatorAuthSessionEngine()) ->exitHighSecurity($viewer, $session); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('session/downgrade/')); } return $this->newDialog() ->setTitle(pht('Leaving High Security')) ->appendParagraph( pht( 'Leave high security and return your session to normal '. 'security levels?')) ->appendParagraph( pht( 'If you leave high security, you will need to authenticate '. 'again the next time you try to take a high security action.')) ->appendParagraph( pht( 'On the plus side, that purple notification bubble will '. 'disappear.')) ->addSubmitButton(pht('Leave High Security')) ->addCancelButton($panel_uri, pht('Stay')); } } diff --git a/src/applications/auth/controller/PhabricatorAuthFinishController.php b/src/applications/auth/controller/PhabricatorAuthFinishController.php index 82f4d72b26..387679b44e 100644 --- a/src/applications/auth/controller/PhabricatorAuthFinishController.php +++ b/src/applications/auth/controller/PhabricatorAuthFinishController.php @@ -1,84 +1,83 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); // If the user already has a full session, just kick them out of here. $has_partial_session = $viewer->hasSession() && $viewer->getSession()->getIsPartial(); if (!$has_partial_session) { return id(new AphrontRedirectResponse())->setURI('/'); } $engine = new PhabricatorAuthSessionEngine(); // If this cookie is set, the user is headed into a high security area // after login (normally because of a password reset) so if they are // able to pass the checkpoint we just want to put their account directly // into high security mode, rather than prompt them again for the same // set of credentials. $jump_into_hisec = $request->getCookie(PhabricatorCookies::COOKIE_HISEC); try { $token = $engine->requireHighSecuritySession( $viewer, $request, '/logout/', $jump_into_hisec); } catch (PhabricatorAuthHighSecurityRequiredException $ex) { $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( $ex->getFactors(), $ex->getFactorValidationResults(), $viewer, $request); return $this->newDialog() ->setTitle(pht('Provide Multi-Factor Credentials')) ->setShortTitle(pht('Multi-Factor Login')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addHiddenInput(AphrontRequest::TYPE_HISEC, true) ->appendParagraph( pht( 'Welcome, %s. To complete the login process, provide your '. 'multi-factor credentials.', phutil_tag('strong', array(), $viewer->getUsername()))) ->appendChild($form->buildLayoutView()) ->setSubmitURI($request->getPath()) ->addCancelButton($ex->getCancelURI()) ->addSubmitButton(pht('Continue')); } // Upgrade the partial session to a full session. $engine->upgradePartialSession($viewer); // TODO: It might be nice to add options like "bind this session to my IP" // here, even for accounts without multi-factor auth attached to them. $next = PhabricatorCookies::getNextURICookie($request); $request->clearCookie(PhabricatorCookies::COOKIE_NEXTURI); $request->clearCookie(PhabricatorCookies::COOKIE_HISEC); if (!PhabricatorEnv::isValidLocalURIForLink($next)) { $next = '/'; } return id(new AphrontRedirectResponse())->setURI($next); } } diff --git a/src/applications/auth/controller/PhabricatorAuthLinkController.php b/src/applications/auth/controller/PhabricatorAuthLinkController.php index 75d63004b4..d50bcf1d8a 100644 --- a/src/applications/auth/controller/PhabricatorAuthLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthLinkController.php @@ -1,137 +1,130 @@ providerKey = $data['pkey']; - $this->action = $data['action']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $action = $request->getURIData('action'); + $provider_key = $request->getURIData('pkey'); $provider = PhabricatorAuthProvider::getEnabledProviderByKey( - $this->providerKey); + $provider_key); if (!$provider) { return new Aphront404Response(); } - switch ($this->action) { + 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()); - switch ($this->action) { + switch ($action) { case 'link': if ($account) { 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) { 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 ($this->action) { + 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 ($this->action) { + 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)); return $this->buildApplicationPage( array( $crumbs, $form, ), array( 'title' => $title, )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index e3cbeaa2c6..65d462cb8e 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -1,254 +1,250 @@ providerKey = $data['pkey']; - $this->extraURIData = idx($data, 'extra'); - } - public function getExtraURIData() { return $this->extraURIData; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + 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; } $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 login is already '. 'associated with another Phabricator user account. Login 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()) { 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 ($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::digest($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 login 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(); $crumbs->setBorder(true); if ($this->getRequest()->getUser()->isLoggedIn()) { $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI()); } else { $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/')); } $crumbs->addTextCrumb($provider->getProviderName()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => pht('Login'), )); } public function buildProviderErrorResponse( PhabricatorAuthProvider $provider, $message) { $message = pht( 'Authentication provider ("%s") encountered an error during login. %s', $provider->getProviderName(), $message); return $this->renderError($message); } } diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php b/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php index 8e0bf99551..0d07470560 100644 --- a/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php +++ b/src/applications/auth/controller/PhabricatorAuthNeedsApprovalController.php @@ -1,39 +1,38 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $wait_for_approval = pht( "Your account has been created, but needs to be approved by an ". "administrator. You'll receive an email once your account is approved."); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Wait for Approval')) ->appendChild($wait_for_approval) ->addCancelButton('/', pht('Wait Patiently')); return $this->buildApplicationPage( $dialog, array( 'title' => pht('Wait For Approval'), )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php index 975355ec97..aaf3864156 100644 --- a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php +++ b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php @@ -1,91 +1,90 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $panel = id(new PhabricatorMultiFactorSettingsPanel()) ->setUser($viewer) ->setViewer($viewer) ->setOverrideURI($this->getApplicationURI('/multifactor/')) ->processRequest($request); if ($panel instanceof AphrontResponse) { return $panel; } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Add Multi-Factor Auth')); $viewer->updateMultiFactorEnrollment(); if (!$viewer->getIsEnrolledInMultiFactor()) { $help = id(new PHUIInfoView()) ->setTitle(pht('Add Multi-Factor Authentication To Your Account')) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors( array( pht( 'Before you can use Phabricator, you need to add multi-factor '. 'authentication to your account.'), pht( 'Multi-factor authentication helps secure your account by '. 'making it more difficult for attackers to gain access or '. 'take sensitive actions.'), pht( 'To learn more about multi-factor authentication, click the '. '%s button below.', phutil_tag('strong', array(), pht('Help'))), pht( 'To add an authentication factor, click the %s button below.', phutil_tag('strong', array(), pht('Add Authentication Factor'))), pht( 'To continue, add at least one authentication factor to your '. 'account.'), )); } else { $help = id(new PHUIInfoView()) ->setTitle(pht('Multi-Factor Authentication Configured')) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setErrors( array( pht( 'You have successfully configured multi-factor authentication '. 'for your account.'), pht( 'You can make adjustments from the Settings panel later.'), pht( 'When you are ready, %s.', phutil_tag( 'strong', array(), phutil_tag( 'a', array( 'href' => '/', ), pht('continue to Phabricator')))), )); } return $this->buildApplicationPage( array( $crumbs, $help, $panel, ), array( 'title' => pht('Add Multi-Factor Authentication'), )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php b/src/applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php index cc7f362583..6b75b929ab 100644 --- a/src/applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php +++ b/src/applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php @@ -1,45 +1,41 @@ provider = $data['provider']; - } - - public function processRequest() { + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $provider = $request->getURIData('provider'); // TODO: Most OAuth providers are OK with changing the redirect URI, but // Google and GitHub are strict. We need to respect the old OAuth URI until // we can get installs to migrate. This just keeps the old OAuth URI working // by redirecting to the new one. $provider_map = array( 'google' => 'google:google.com', 'github' => 'github:github.com', ); - if (!isset($provider_map[$this->provider])) { + if (!isset($provider_map[$provider])) { return new Aphront404Response(); } - $provider_key = $provider_map[$this->provider]; + $provider_key = $provider_map[$provider]; $uri = $this->getRequest()->getRequestURI(); $uri->setPath($this->getApplicationURI('login/'.$provider_key.'/')); return id(new AphrontRedirectResponse())->setURI($uri); } } diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index 312367d03a..6dfa49d860 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -1,199 +1,191 @@ linkType = $data['type']; - $this->id = $data['id']; - $this->key = $data['key']; - $this->emailID = idx($data, 'emailID'); - } - - public function processRequest() { - $request = $this->getRequest(); + 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($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$target_user) { return new Aphront404Response(); } // 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 ($this->emailID) { + if ($email_id) { $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND id = %d', $target_user->getPHID(), - $this->emailID); + $email_id); if (!$target_email) { return new Aphront404Response(); } } $engine = new PhabricatorAuthSessionEngine(); $token = $engine->loadOneTimeLoginKey( $target_user, $target_email, - $this->key); + $key); if (!$token) { return $this->newDialog() ->setTitle(pht('Unable to Login')) ->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 ($request->isFormPost()) { // 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. $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. PhabricatorAuthTemporaryToken::revokeTokens( $target_user, array($target_user->getPHID()), array( PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE, PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE, )); if ($target_email) { id(new PhabricatorUserEditor()) ->setActor($target_user) ->verifyEmail($target_user, $target_email); } unset($unguarded); $next = '/'; if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { $next = '/settings/panel/external/'; } else { // 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); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); id(new PhabricatorAuthTemporaryToken()) ->setObjectPHID($target_user->getPHID()) ->setTokenType( PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE) ->setTokenExpires(time() + phutil_units('1 hour in seconds')) ->setTokenCode(PhabricatorHash::digest($key)) ->save(); unset($unguarded); $next = (string)id(new PhutilURI('/settings/panel/password/')) ->setQueryParams( array( 'key' => $key, )); $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); } PhabricatorCookies::setNextURICookie($request, $next, $force = true); return $this->loginUser($target_user); } // 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 ($this->linkType) { + 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('Login 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('Login (%s)', $target_user->getUsername())) ->addCancelButton('/'); foreach ($body as $paragraph) { $dialog->appendParagraph($paragraph); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 9341345143..655f63acb9 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -1,656 +1,651 @@ accountKey = idx($data, 'akey'); - } - - public function processRequest() { - $request = $this->getRequest(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $account_key = $request->getURIData('akey'); if ($request->getUser()->isLoggedIn()) { return $this->renderError(pht('You are already logged in.')); } $is_setup = false; - if (strlen($this->accountKey)) { - $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); + 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(); $is_default = true; $is_setup = true; } else { list($account, $provider, $response) = $this->loadDefaultAccount(); $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. 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())); } } $user = new PhabricatorUser(); $default_username = $account->getUsername(); $default_realname = $account->getRealName(); $default_email = $account->getEmail(); if ($invite) { $default_email = $invite->getEmailAddress(); } if (!PhabricatorUserEmail::isValidAddress($default_email)) { $default_email = null; } if ($default_email !== null) { // We should bypass policy here becase 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) { $default_email = 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)) { 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:', $default_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. // TODO: See T3472. 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 { $default_email = null; } } } } $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(); $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; $errors = array(); $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; } if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) { $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'); if (!strlen($value_password)) { $e_password = pht('Required'); $errors[] = pht('You must choose a password.'); } else if ($value_password !== $value_confirm) { $e_password = pht('No Match'); $errors[] = pht('Password and confirmation must match.'); } else if (strlen($value_password) < $min_len) { $e_password = pht('Too Short'); $errors[] = pht( 'Password is too short (must be at least %d characters long).', $min_len); } else if ( PhabricatorCommonPasswords::isCommonPassword($value_password)) { $e_password = pht('Very Weak'); $errors[] = pht( 'Password is pathologically weak. This password is one of the '. 'most common passwords in use, and is extremely easy for '. 'attackers to guess. You must choose a stronger password.'); } else { $e_password = null; } } 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()); } try { $verify_email = false; if ($force_verify) { $verify_email = true; } if ($value_email === $default_email) { if ($account->getEmailVerified()) { $verify_email = true; } if ($provider->shouldTrustEmails()) { $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) { $envelope = new PhutilOpaqueEnvelope($value_password); $editor->changePassword($user, $envelope); } if ($is_setup) { $editor->makeAdminUser($user, true); } $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()); 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('Phabricator Username')) ->setName('username') ->setValue($value_username) ->setError($e_username)); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Phabricator 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 Phabricator 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('Phabricator Registration'); } $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.')); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormErrors($errors); $invite_header = null; if ($invite) { $invite_header = $this->renderInviteHeader($invite); } return $this->buildApplicationPage( array( $crumbs, $welcome_view, $invite_header, $object_box, ), array( 'title' => $title, )); } private function loadDefaultAccount() { $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $account = null; $provider = null; $response = null; foreach ($providers as $key => $candidate_provider) { 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(); 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/PhabricatorAuthRevokeTokenController.php b/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php index 27981eee27..c1f0c21cb1 100644 --- a/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php +++ b/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php @@ -1,78 +1,72 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $is_all = ($this->id === 'all'); + $is_all = ($id === 'all'); $query = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($viewer->getPHID())); if (!$is_all) { - $query->withIDs(array($this->id)); + $query->withIDs(array($id)); } $tokens = $query->execute(); foreach ($tokens as $key => $token) { if (!$token->isRevocable()) { // Don't revoke unrevocable tokens. unset($tokens[$key]); } } $panel_uri = '/settings/panel/tokens/'; if (!$tokens) { return $this->newDialog() ->setTitle(pht('No Matching Tokens')) ->appendParagraph( pht('There are no matching tokens to revoke.')) ->appendParagraph( pht( '(Some types of token can not be revoked, and you can not revoke '. 'tokens which have already expired.)')) ->addCancelButton($panel_uri); } if ($request->isDialogFormPost()) { foreach ($tokens as $token) { $token->revokeToken(); } return id(new AphrontRedirectResponse())->setURI($panel_uri); } if ($is_all) { $title = pht('Revoke Tokens?'); $short = pht('Revoke Tokens'); $body = pht( 'Really revoke all tokens? Among other temporary authorizations, '. 'this will disable any outstanding password reset or account '. 'recovery links.'); } else { $title = pht('Revoke Token?'); $short = pht('Revoke Token'); $body = pht( 'Really revoke this token? Any temporary authorization it enables '. 'will be disabled.'); } return $this->newDialog() ->setTitle($title) ->setShortTitle($short) ->appendParagraph($body) ->addSubmitButton(pht('Revoke')) ->addCancelButton($panel_uri); } } diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php index bb4acd0b4d..d09d52cc14 100644 --- a/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php @@ -1,143 +1,143 @@ getViewer(); - $id = $request->getURIData('id'); + if ($id) { $key = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$key) { return new Aphront404Response(); } $is_new = false; } else { $key = $this->newKeyForObjectPHID($request->getStr('objectPHID')); if (!$key) { return new Aphront404Response(); } $is_new = true; } $cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer); if ($key->getIsTrusted()) { $id = $key->getID(); return $this->newDialog() ->setTitle(pht('Can Not Edit Trusted Key')) ->appendParagraph( pht( 'This key is trusted. Trusted keys can not be edited. '. 'Use %s to revoke trust before editing the key.', phutil_tag( 'tt', array(), "bin/almanac untrust-key --id {$id}"))) ->addCancelButton($cancel_uri, pht('Okay')); } $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $cancel_uri); $v_name = $key->getName(); $e_name = strlen($v_name) ? null : true; $v_key = $key->getEntireKey(); $e_key = strlen($v_key) ? null : true; $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_key = $request->getStr('key'); if (!strlen($v_name)) { $errors[] = pht('You must provide a name for this public key.'); $e_name = pht('Required'); } else { $key->setName($v_name); } if (!strlen($v_key)) { $errors[] = pht('You must provide a public key.'); $e_key = pht('Required'); } else { try { $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($v_key); $type = $public_key->getType(); $body = $public_key->getBody(); $comment = $public_key->getComment(); $key->setKeyType($type); $key->setKeyBody($body); $key->setKeyComment($comment); $e_key = null; } catch (Exception $ex) { $e_key = pht('Invalid'); $errors[] = $ex->getMessage(); } } if (!$errors) { try { $key->save(); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } catch (Exception $ex) { $e_key = pht('Duplicate'); $errors[] = pht( 'This public key is already associated with another user or '. 'device. Each key must unambiguously identify a single unique '. 'owner.'); } } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setError($e_name) ->setValue($v_name)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Public Key')) ->setName('key') ->setValue($v_key) ->setError($e_key)); if ($is_new) { $title = pht('Upload SSH Public Key'); $save_button = pht('Upload Public Key'); $form->addHiddenInput('objectPHID', $key->getObject()->getPHID()); } else { $title = pht('Edit SSH Public Key'); $save_button = pht('Save Changes'); } return $this->newDialog() ->setTitle($title) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setErrors($errors) ->appendForm($form) ->addSubmitButton($save_button) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php b/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php index d2534c4a45..ae8179a798 100644 --- a/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php +++ b/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php @@ -1,80 +1,74 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $is_all = ($this->id === 'all'); + $is_all = ($id === 'all'); $query = id(new PhabricatorAuthSessionQuery()) ->setViewer($viewer) ->withIdentityPHIDs(array($viewer->getPHID())); if (!$is_all) { - $query->withIDs(array($this->id)); + $query->withIDs(array($id)); } $current_key = PhabricatorHash::digest( $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); $sessions = $query->execute(); foreach ($sessions as $key => $session) { if ($session->getSessionKey() == $current_key) { // Don't terminate the current login session. unset($sessions[$key]); } } $panel_uri = '/settings/panel/sessions/'; if (!$sessions) { return $this->newDialog() ->setTitle(pht('No Matching Sessions')) ->appendParagraph( pht('There are no matching sessions to terminate.')) ->appendParagraph( pht( '(You can not terminate your current login session. To '. 'terminate it, log out.)')) ->addCancelButton($panel_uri); } if ($request->isDialogFormPost()) { foreach ($sessions as $session) { $session->delete(); } return id(new AphrontRedirectResponse())->setURI($panel_uri); } if ($is_all) { $title = pht('Terminate Sessions?'); $short = pht('Terminate Sessions'); $body = pht( 'Really terminate all sessions? (Your current login session will '. 'not be terminated.)'); } else { $title = pht('Terminate Session?'); $short = pht('Terminate Session'); $body = pht( 'Really terminate session %s?', phutil_tag('strong', array(), substr($session->getSessionKey(), 0, 6))); } return $this->newDialog() ->setTitle($title) ->setShortTitle($short) ->appendParagraph($body) ->addSubmitButton(pht('Terminate')) ->addCancelButton($panel_uri); } } diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php index a5bdf90b70..3f694207b9 100644 --- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php @@ -1,149 +1,145 @@ providerKey = $data['pkey']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + 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()); if (!$account) { return $this->renderNoAccountErrorDialog(); } // 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); } } // Check that this account isn't the last account which can be used to // login. We prevent you from removing the last account. if ($account->isUsableForLogin()) { $other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $viewer->getPHID()); $valid_accounts = 0; foreach ($other_accounts as $other_account) { if ($other_account->isUsableForLogin()) { $valid_accounts++; } } if ($valid_accounts < 2) { return $this->renderLastUsableAccountErrorDialog(); } } if ($request->isDialogFormPost()) { $account->delete(); id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $viewer, $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); } return $this->renderConfirmDialog($account); } 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()); return id(new AphrontDialogResponse())->setDialog($dialog); } private function renderNotUnlinkableErrorDialog( PhabricatorAuthProvider $provider) { $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->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.', $provider->getProviderName())) ->addCancelButton($this->getDoneURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } 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 login. Add another authentication method before removing '. 'this one.')) ->addCancelButton($this->getDoneURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } 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.'); } $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setTitle($title) ->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); } } diff --git a/src/applications/auth/controller/PhabricatorAuthValidateController.php b/src/applications/auth/controller/PhabricatorAuthValidateController.php index c91f3c4504..bb45a68acf 100644 --- a/src/applications/auth/controller/PhabricatorAuthValidateController.php +++ b/src/applications/auth/controller/PhabricatorAuthValidateController.php @@ -1,76 +1,75 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $failures = array(); if (!strlen($request->getStr('expect'))) { return $this->renderErrors( array( pht( 'Login validation is missing expected parameter ("%s").', 'phusr'), )); } $expect_phusr = $request->getStr('expect'); $actual_phusr = $request->getCookie(PhabricatorCookies::COOKIE_USERNAME); if ($actual_phusr != $expect_phusr) { if ($actual_phusr) { $failures[] = pht( "Attempted to set '%s' cookie to '%s', but your browser sent back ". "a cookie with the value '%s'. Clear your browser's cookies and ". "try again.", 'phusr', $expect_phusr, $actual_phusr); } else { $failures[] = pht( "Attempted to set '%s' cookie to '%s', but your browser did not ". "accept the cookie. Check that cookies are enabled, clear them, ". "and try again.", 'phusr', $expect_phusr); } } if (!$failures) { if (!$viewer->getPHID()) { $failures[] = pht( 'Login cookie was set correctly, but your login session is not '. 'valid. Try clearing cookies and logging in again.'); } } if ($failures) { return $this->renderErrors($failures); } $finish_uri = $this->getApplicationURI('finish/'); return id(new AphrontRedirectResponse())->setURI($finish_uri); } private function renderErrors(array $messages) { return $this->renderErrorPage( pht('Login Failure'), $messages); } } diff --git a/src/applications/auth/controller/PhabricatorDisabledUserController.php b/src/applications/auth/controller/PhabricatorDisabledUserController.php index 842f2daad6..39e390d44a 100644 --- a/src/applications/auth/controller/PhabricatorDisabledUserController.php +++ b/src/applications/auth/controller/PhabricatorDisabledUserController.php @@ -1,24 +1,25 @@ getRequest(); - $user = $request->getUser(); - if (!$user->getIsDisabled()) { + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + + if (!$viewer->getIsDisabled()) { return new Aphront404Response(); } return id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Account Disabled')) ->addCancelButton('/logout/', pht('Okay')) ->appendParagraph(pht('Your account has been disabled.')); } } diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 9db360d51d..26609133ea 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -1,166 +1,165 @@ getRequest(); + public function handleRequest(AphrontRequest $request) { if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { return new Aphront400Response(); } $e_email = true; $e_captcha = true; $errors = array(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 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'); } $email = $request->getStr('email'); if (!strlen($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); $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. You can only send '. 'password reset links to a verified address.'); $e_email = pht('Unverified'); } } if (!$errors) { $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $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); } $mail = id(new PhabricatorMetaMTAMail()) ->setSubject(pht('[Phabricator] Password Reset')) ->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 login.')) ->addCancelButton('/', pht('Done')); } } } $error_view = null; if ($errors) { $error_view = new PHUIInfoView(); $error_view->setErrors($errors); } $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( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Reset Password')); $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/'); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Forgot Password'), )); } } diff --git a/src/applications/auth/controller/PhabricatorEmailVerificationController.php b/src/applications/auth/controller/PhabricatorEmailVerificationController.php index ea5f273d79..83a370139c 100644 --- a/src/applications/auth/controller/PhabricatorEmailVerificationController.php +++ b/src/applications/auth/controller/PhabricatorEmailVerificationController.php @@ -1,97 +1,91 @@ code = $data['code']; - } - public function shouldRequireEmailVerification() { // Since users need to be able to hit this endpoint in order to verify // email, we can't ever require email verification here. return false; } public function shouldRequireEnabledUser() { // Unapproved users are allowed to verify their email addresses. We'll kick // disabled users out later. return false; } - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $code = $request->getURIData('code'); - if ($user->getIsDisabled()) { + if ($viewer->getIsDisabled()) { // We allowed unapproved and disabled users to hit this controller, but // want to kick out disabled users now. return new Aphront400Response(); } $email = id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND verificationCode = %s', - $user->getPHID(), - $this->code); + $viewer->getPHID(), + $code); $submit = null; if (!$email) { $title = pht('Unable to Verify Email'); $content = pht( 'The verification code you provided is incorrect, or the email '. 'address has been removed, or the email address is owned by another '. 'user. Make sure you followed the link in the email correctly and are '. 'logged in with the user account associated with the email address.'); $continue = pht('Rats!'); - } else if ($email->getIsVerified() && $user->getIsEmailVerified()) { + } else if ($email->getIsVerified() && $viewer->getIsEmailVerified()) { $title = pht('Address Already Verified'); $content = pht( 'This email address has already been verified.'); $continue = pht('Continue to Phabricator'); } else if ($request->isFormPost()) { id(new PhabricatorUserEditor()) - ->setActor($user) - ->verifyEmail($user, $email); + ->setActor($viewer) + ->verifyEmail($viewer, $email); $title = pht('Address Verified'); $content = pht( 'The email address %s is now verified.', phutil_tag('strong', array(), $email->getAddress())); $continue = pht('Continue to Phabricator'); } else { $title = pht('Verify Email Address'); $content = pht( 'Verify this email address (%s) and attach it to your account?', phutil_tag('strong', array(), $email->getAddress())); $continue = pht('Cancel'); $submit = pht('Verify %s', $email->getAddress()); } $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle($title) ->addCancelButton('/', $continue) ->appendChild($content); if ($submit) { $dialog->addSubmitButton($submit); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Verify Email')); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Verify Email'), )); } } diff --git a/src/applications/auth/controller/PhabricatorLogoutController.php b/src/applications/auth/controller/PhabricatorLogoutController.php index 127e5b5e1f..de3ac50e5d 100644 --- a/src/applications/auth/controller/PhabricatorLogoutController.php +++ b/src/applications/auth/controller/PhabricatorLogoutController.php @@ -1,73 +1,72 @@ getRequest(); - $user = $request->getUser(); + $viewer = $this->getViewer(); if ($request->isFormPost()) { $log = PhabricatorUserLog::initializeNewLog( - $user, - $user->getPHID(), + $viewer, + $viewer->getPHID(), PhabricatorUserLog::ACTION_LOGOUT); $log->save(); // Destroy the user's session in the database so logout works even if // their cookies have some issues. We'll detect cookie issues when they // try to login again and tell them to clear any junk. $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); if (strlen($phsid)) { $session = id(new PhabricatorAuthSessionQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withSessionKeys(array($phsid)) ->executeOne(); if ($session) { $session->delete(); } } $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); return id(new AphrontRedirectResponse()) ->setURI('/auth/loggedout/'); } - if ($user->getPHID()) { + if ($viewer->getPHID()) { $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Log out of Phabricator?')) ->appendChild(pht('Are you sure you want to log out?')) ->addSubmitButton(pht('Logout')) ->addCancelButton('/'); return id(new AphrontDialogResponse())->setDialog($dialog); } return id(new AphrontRedirectResponse())->setURI('/'); } } diff --git a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php index e64096e4be..779196382d 100644 --- a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php +++ b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php @@ -1,67 +1,66 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); - $email = $user->loadPrimaryEmail(); + $email = $viewer->loadPrimaryEmail(); - if ($user->getIsEmailVerified()) { + if ($viewer->getIsEmailVerified()) { return id(new AphrontRedirectResponse())->setURI('/'); } $email_address = $email->getAddress(); $sent = null; if ($request->isFormPost()) { - $email->sendVerificationEmail($user); + $email->sendVerificationEmail($viewer); $sent = new PHUIInfoView(); $sent->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $sent->setTitle(pht('Email Sent')); $sent->appendChild( pht( 'Another verification email was sent to %s.', phutil_tag('strong', array(), $email_address))); } $must_verify = pht( 'You must verify your email address to login. You should have a '. 'new email message from Phabricator with verification instructions '. 'in your inbox (%s).', phutil_tag('strong', array(), $email_address)); $send_again = pht( 'If you did not receive an email, you can click the button below '. 'to try sending another one.'); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Check Your Email')) ->appendParagraph($must_verify) ->appendParagraph($send_again) ->addSubmitButton(pht('Send Another Email')); return $this->buildApplicationPage( array( $sent, $dialog, ), array( 'title' => pht('Must Verify Email'), )); } } diff --git a/src/applications/auth/controller/PhabricatorRefreshCSRFController.php b/src/applications/auth/controller/PhabricatorRefreshCSRFController.php index 19d7aa7eb1..fc1d5cc02d 100644 --- a/src/applications/auth/controller/PhabricatorRefreshCSRFController.php +++ b/src/applications/auth/controller/PhabricatorRefreshCSRFController.php @@ -1,16 +1,15 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); return id(new AphrontAjaxResponse()) ->setContent( array( - 'token' => $user->getCSRFToken(), + 'token' => $viewer->getCSRFToken(), )); } } diff --git a/src/applications/badges/editor/PhabricatorBadgesEditor.php b/src/applications/badges/editor/PhabricatorBadgesEditor.php index d060c6df7a..a6cd90bb92 100644 --- a/src/applications/badges/editor/PhabricatorBadgesEditor.php +++ b/src/applications/badges/editor/PhabricatorBadgesEditor.php @@ -1,194 +1,212 @@ getTransactionType()) { case PhabricatorBadgesTransaction::TYPE_NAME: return $object->getName(); case PhabricatorBadgesTransaction::TYPE_FLAVOR: return $object->getFlavor(); case PhabricatorBadgesTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhabricatorBadgesTransaction::TYPE_ICON: return $object->getIcon(); case PhabricatorBadgesTransaction::TYPE_QUALITY: return $object->getQuality(); case PhabricatorBadgesTransaction::TYPE_STATUS: return $object->getStatus(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorBadgesTransaction::TYPE_NAME: case PhabricatorBadgesTransaction::TYPE_FLAVOR: case PhabricatorBadgesTransaction::TYPE_DESCRIPTION: case PhabricatorBadgesTransaction::TYPE_ICON: case PhabricatorBadgesTransaction::TYPE_STATUS: case PhabricatorBadgesTransaction::TYPE_QUALITY: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorBadgesTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; case PhabricatorBadgesTransaction::TYPE_FLAVOR: $object->setFlavor($xaction->getNewValue()); return; case PhabricatorBadgesTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; case PhabricatorBadgesTransaction::TYPE_ICON: $object->setIcon($xaction->getNewValue()); return; case PhabricatorBadgesTransaction::TYPE_QUALITY: $object->setQuality($xaction->getNewValue()); return; case PhabricatorBadgesTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorBadgesTransaction::TYPE_NAME: case PhabricatorBadgesTransaction::TYPE_FLAVOR: case PhabricatorBadgesTransaction::TYPE_DESCRIPTION: case PhabricatorBadgesTransaction::TYPE_ICON: case PhabricatorBadgesTransaction::TYPE_STATUS: case PhabricatorBadgesTransaction::TYPE_QUALITY: return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorBadgesTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Badge name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; } return $errors; } + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + public function getMailTagsMap() { + return array( + PhabricatorBadgesTransaction::MAILTAG_DETAILS => + pht('Someone changes the badge\'s details.'), + PhabricatorBadgesTransaction::MAILTAG_COMMENT => + pht('Someone comments on a badge.'), + PhabricatorBadgesTransaction::MAILTAG_OTHER => + pht('Other badge activity not listed above occurs.'), + ); + } + protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { - return id(new PhabricatorMacroReplyHandler()) + return id(new PhabricatorBadgesReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $name = $object->getName(); $id = $object->getID(); $name = pht('Badge %d', $id); return id(new PhabricatorMetaMTAMail()) ->setSubject($name) ->addHeader('Thread-Topic', $name); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( + $object->getCreatorPHID(), $this->requireActor()->getPHID(), ); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $description = $object->getDescription(); $body = parent::buildMailBody($object, $xactions); if (strlen($description)) { $body->addTextSection( pht('BADGE DESCRIPTION'), $object->getDescription()); } $body->addLinkSection( pht('BADGE DETAIL'), PhabricatorEnv::getProductionURI('/badge/view/'.$object->getID().'/')); return $body; } protected function getMailSubjectPrefix() { return pht('[Badge]'); } } diff --git a/src/applications/badges/mail/PhabricatorBadgesMailReceiver.php b/src/applications/badges/mail/PhabricatorBadgesMailReceiver.php new file mode 100644 index 0000000000..4e72c8422d --- /dev/null +++ b/src/applications/badges/mail/PhabricatorBadgesMailReceiver.php @@ -0,0 +1,28 @@ +setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + } + + protected function getTransactionReplyHandler() { + return new PhabricatorBadgesReplyHandler(); + } + +} diff --git a/src/applications/badges/mail/PhabricatorBadgesReplyHandler.php b/src/applications/badges/mail/PhabricatorBadgesReplyHandler.php new file mode 100644 index 0000000000..af03dd7e2a --- /dev/null +++ b/src/applications/badges/mail/PhabricatorBadgesReplyHandler.php @@ -0,0 +1,16 @@ + pht('Active'), self::STATUS_CLOSED => pht('Archived'), ); } public static function getQualityNameMap() { return array( self::POOR => pht('Poor'), self::COMMON => pht('Common'), self::UNCOMMON => pht('Uncommon'), self::RARE => pht('Rare'), self::EPIC => pht('Epic'), self::LEGENDARY => pht('Legendary'), self::HEIRLOOM => pht('Heirloom'), ); } public static function getIconNameMap() { return PhabricatorBadgesIcon::getIconMap(); } public static function initializeNewBadge(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorBadgesApplication')) ->executeOne(); $view_policy = PhabricatorPolicies::getMostOpenPolicy(); $edit_policy = $app->getPolicy(PhabricatorBadgesDefaultEditCapability::CAPABILITY); return id(new PhabricatorBadgesBadge()) ->setIcon(self::DEFAULT_ICON) ->setQuality(self::DEFAULT_QUALITY) ->setCreatorPHID($actor->getPHID()) - ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setStatus(self::STATUS_OPEN); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'flavor' => 'text255', 'description' => 'text', 'icon' => 'text255', 'quality' => 'text255', 'status' => 'text32', + 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_creator' => array( 'columns' => array('creatorPHID', 'dateModified'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PhabricatorBadgesPHIDType::TYPECONST); } public function isClosed() { return ($this->getStatus() == self::STATUS_CLOSED); } public function attachRecipientPHIDs(array $phids) { $this->recipientPHIDs = $phids; return $this; } public function getRecipientPHIDs() { return $this->assertAttached($this->recipientPHIDs); } + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + /* -( 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; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorBadgesEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorBadgesTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->creatorPHID == $phid); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } +/* -( PhabricatorTokenReceiverInterface )---------------------------------- */ + + + public function getUsersToNotifyOfTokenGiven() { + return array($this->getCreatorPHID()); + } + + + /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/badges/storage/PhabricatorBadgesTransaction.php b/src/applications/badges/storage/PhabricatorBadgesTransaction.php index e7ab4eab6a..c50297f768 100644 --- a/src/applications/badges/storage/PhabricatorBadgesTransaction.php +++ b/src/applications/badges/storage/PhabricatorBadgesTransaction.php @@ -1,196 +1,223 @@ getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created this badge.', $this->renderHandleLink($author_phid)); } else { return pht( '%s renamed this badge from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_FLAVOR: if ($old === null) { return pht( '%s set the flavor text for this badge.', $this->renderHandleLink($author_phid)); } else { return pht( '%s updated the flavor text for this badge.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_DESCRIPTION: if ($old === null) { return pht( '%s set the description for this badge.', $this->renderHandleLink($author_phid)); } else { return pht( '%s updated the description for this badge.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_ICON: if ($old === null) { return pht( '%s set the icon for this badge as "%s".', $this->renderHandleLink($author_phid), $new); } else { $icon_map = PhabricatorBadgesBadge::getIconNameMap(); $icon_new = idx($icon_map, $new, $new); $icon_old = idx($icon_map, $old, $old); return pht( '%s updated the icon for this badge from "%s" to "%s".', $this->renderHandleLink($author_phid), $icon_old, $icon_new); } break; case self::TYPE_QUALITY: if ($old === null) { return pht( '%s set the quality for this badge as "%s".', $this->renderHandleLink($author_phid), $new); } else { $qual_map = PhabricatorBadgesBadge::getQualityNameMap(); $qual_new = idx($qual_map, $new, $new); $qual_old = idx($qual_map, $old, $old); return pht( '%s updated the quality for this badge from "%s" to "%s".', $this->renderHandleLink($author_phid), $qual_old, $qual_new); } break; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s renamed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; case self::TYPE_FLAVOR: return pht( '%s updated the flavor text for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_ICON: return pht( '%s updated the icon for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_QUALITY: return pht( '%s updated the quality level for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_DESCRIPTION: return pht( '%s updated the description for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_STATUS: switch ($new) { case PhabricatorBadgesBadge::STATUS_OPEN: return pht( '%s activated %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorBadgesBadge::STATUS_CLOSED: return pht( '%s archived %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; } return parent::getTitleForFeed(); } + public function getMailTags() { + $tags = parent::getMailTags(); + + switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_COMMENT: + $tags[] = self::MAILTAG_COMMENT; + break; + case self::TYPE_NAME: + case self::TYPE_DESCRIPTION: + case self::TYPE_FLAVOR: + case self::TYPE_ICON: + case self::TYPE_STATUS: + case self::TYPE_QUALITY: + $tags[] = self::MAILTAG_DETAILS; + break; + default: + $tags[] = self::MAILTAG_OTHER; + break; + } + return $tags; + } + public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return ($old === null); } return parent::shouldHide(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php index 8cbd594bea..4be5a77d11 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php @@ -1,129 +1,123 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $sequence = $request->getURIData('sequence'); $event = id(new PhabricatorCalendarEventQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if ($sequence) { $parent_event = $event; - $event = $parent_event->generateNthGhost($sequence, $user); + $event = $parent_event->generateNthGhost($sequence, $viewer); $event->attachParentEvent($parent_event); } if (!$event) { return new Aphront404Response(); } if (!$sequence) { $cancel_uri = '/E'.$event->getID(); } else { $cancel_uri = '/E'.$event->getID().'/'.$sequence; } $is_cancelled = $event->getIsCancelled(); $is_parent_cancelled = $event->getIsParentCancelled(); $is_parent = $event->getIsRecurrenceParent(); $validation_exception = null; if ($request->isFormPost()) { if ($is_cancelled && $sequence) { return id(new AphrontRedirectResponse())->setURI($cancel_uri); } else if ($sequence) { $event = $this->createEventFromGhost( - $user, + $viewer, $event, $sequence); - $event->applyViewerTimezone($user); + $event->applyViewerTimezone($viewer); } $xactions = array(); $xaction = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_CANCEL) ->setNewValue(!$is_cancelled); $editor = id(new PhabricatorCalendarEventEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); try { $editor->applyTransactions($event, array($xaction)); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } if ($is_cancelled) { if ($sequence || $is_parent_cancelled) { $title = pht('Cannot Reinstate Instance'); $paragraph = pht( 'Cannot reinstate an instance of a cancelled recurring event.'); $cancel = pht('Cancel'); $submit = null; } else if ($is_parent) { $title = pht('Reinstate Recurrence'); $paragraph = pht( 'Reinstate the entire series of recurring events?'); $cancel = pht("Don't Reinstate Recurrence"); $submit = pht('Reinstate Recurrence'); } else { $title = pht('Reinstate Event'); $paragraph = pht('Reinstate this event?'); $cancel = pht("Don't Reinstate Event"); $submit = pht('Reinstate Event'); } } else { if ($sequence) { $title = pht('Cancel Instance'); $paragraph = pht( 'Cancel just this instance of a recurring event.'); $cancel = pht("Don't Cancel Instance"); $submit = pht('Cancel Instance'); } else if ($is_parent) { $title = pht('Cancel Recurrence'); $paragraph = pht( 'Cancel the entire series of recurring events?'); $cancel = pht("Don't Cancel Recurrence"); $submit = pht('Cancel Recurrence'); } else { $title = pht('Cancel Event'); $paragraph = pht( 'You can always reinstate the event later.'); $cancel = pht("Don't Cancel Event"); $submit = pht('Cancel Event'); } } return $this->newDialog() ->setTitle($title) ->setValidationException($validation_exception) ->appendParagraph($paragraph) ->addCancelButton($cancel_uri, $cancel) ->addSubmitButton($submit); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php index 9094de5401..876712a455 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php @@ -1,85 +1,81 @@ id = idx($data, 'id'); - } - public function handleRequest(AphrontRequest $request) { if (!$request->isFormPost()) { return new Aphront400Response(); } - $user = $request->getUser(); + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $event = id(new PhabricatorCalendarEventQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$event) { return new Aphront404Response(); } $index = $request->getURIData('sequence'); if ($index && !$is_preview) { $result = $this->getEventAtIndexForGhostPHID( - $user, + $viewer, $event->getPHID(), $index); if ($result) { $event = $result; } else { $event = $this->createEventFromGhost( - $user, + $viewer, $event, $index); - $event->applyViewerTimezone($user); + $event->applyViewerTimezone($viewer); } } $view_uri = '/'.$event->getMonogram(); $xactions = array(); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorCalendarEventTransactionComment()) ->setContent($request->getStr('comment'))); $editor = id(new PhabricatorCalendarEventEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($event, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index 07da53fa67..e55b0debaa 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -1,637 +1,635 @@ id = idx($data, 'id'); - } - public function isCreate() { return !$this->id; } public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $user_phid = $viewer->getPHID(); + $this->id = $request->getURIData('id'); + $error_name = true; $error_recurrence_end_date = null; $error_start_date = true; $error_end_date = true; $validation_exception = null; $is_recurring_id = celerity_generate_unique_node_id(); $recurrence_end_date_id = celerity_generate_unique_node_id(); $frequency_id = celerity_generate_unique_node_id(); $all_day_id = celerity_generate_unique_node_id(); $start_date_id = celerity_generate_unique_node_id(); $end_date_id = celerity_generate_unique_node_id(); $next_workflow = $request->getStr('next'); $uri_query = $request->getStr('query'); if ($this->isCreate()) { $mode = $request->getStr('mode'); $event = PhabricatorCalendarEvent::initializeNewCalendarEvent( $viewer, $mode); $create_start_year = $request->getInt('year'); $create_start_month = $request->getInt('month'); $create_start_day = $request->getInt('day'); $create_start_time = $request->getStr('time'); if ($create_start_year) { $start = AphrontFormDateControlValue::newFromParts( $viewer, $create_start_year, $create_start_month, $create_start_day, $create_start_time); if (!$start->isValid()) { return new Aphront400Response(); } $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $start->getEpoch()); $end = clone $start_value->getDateTime(); $end->modify('+1 hour'); $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $end->format('U')); } else { list($start_value, $end_value) = $this->getDefaultTimeValues($viewer); } $recurrence_end_date_value = clone $end_value; $recurrence_end_date_value->setOptional(true); $submit_label = pht('Create'); $page_title = pht('Create Event'); $redirect = 'created'; $subscribers = array(); $invitees = array($user_phid); $cancel_uri = $this->getApplicationURI(); } else { $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$event) { return new Aphront404Response(); } if ($request->getURIData('sequence')) { $index = $request->getURIData('sequence'); $result = $this->getEventAtIndexForGhostPHID( $viewer, $event->getPHID(), $index); if ($result) { return id(new AphrontRedirectResponse()) ->setURI('/calendar/event/edit/'.$result->getID().'/'); } $event = $this->createEventFromGhost( $viewer, $event, $index); return id(new AphrontRedirectResponse()) ->setURI('/calendar/event/edit/'.$event->getID().'/'); } $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getDateTo()); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getDateFrom()); $recurrence_end_date_value = id(clone $end_value) ->setOptional(true); $submit_label = pht('Update'); $page_title = pht('Update Event'); $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $event->getPHID()); $invitees = array(); foreach ($event->getInvitees() as $invitee) { if ($invitee->isUninvited()) { continue; } else { $invitees[] = $invitee->getInviteePHID(); } } $cancel_uri = '/'.$event->getMonogram(); } if ($this->isCreate()) { $projects = array(); } else { $projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $event->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $projects = array_reverse($projects); } $name = $event->getName(); $description = $event->getDescription(); $is_all_day = $event->getIsAllDay(); $is_recurring = $event->getIsRecurring(); $is_parent = $event->getIsRecurrenceParent(); $frequency = idx($event->getRecurrenceFrequency(), 'rule'); $icon = $event->getIcon(); $edit_policy = $event->getEditPolicy(); $view_policy = $event->getViewPolicy(); $space = $event->getSpacePHID(); if ($request->isFormPost()) { $xactions = array(); $name = $request->getStr('name'); $start_value = AphrontFormDateControlValue::newFromRequest( $request, 'start'); $end_value = AphrontFormDateControlValue::newFromRequest( $request, 'end'); $recurrence_end_date_value = AphrontFormDateControlValue::newFromRequest( $request, 'recurrenceEndDate'); $recurrence_end_date_value->setOptional(true); $projects = $request->getArr('projects'); $description = $request->getStr('description'); $subscribers = $request->getArr('subscribers'); $edit_policy = $request->getStr('editPolicy'); $view_policy = $request->getStr('viewPolicy'); $space = $request->getStr('spacePHID'); $is_recurring = $request->getStr('isRecurring') ? 1 : 0; $frequency = $request->getStr('frequency'); $is_all_day = $request->getStr('isAllDay'); $icon = $request->getStr('icon'); $invitees = $request->getArr('invitees'); $new_invitees = $this->getNewInviteeList($invitees, $event); $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; if ($this->isCreate()) { $status = idx($new_invitees, $viewer->getPHID()); if ($status) { $new_invitees[$viewer->getPHID()] = $status_attending; } } $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_NAME) ->setNewValue($name); if ($is_recurring && $this->isCreate()) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_RECURRING) ->setNewValue($is_recurring); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_FREQUENCY) ->setNewValue(array('rule' => $frequency)); if (!$recurrence_end_date_value->isDisabled()) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE) ->setNewValue($recurrence_end_date_value); } } if (($is_recurring && $this->isCreate()) || !$is_parent) { $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ALL_DAY) ->setNewValue($is_all_day); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ICON) ->setNewValue($icon); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_START_DATE) ->setNewValue($start_value); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_END_DATE) ->setNewValue($end_value); } $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => array_fuse($subscribers))); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_INVITE) ->setNewValue($new_invitees); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION) ->setNewValue($description); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) ->setNewValue($space); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($projects))); $xactions = $editor->applyTransactions($event, $xactions); $response = id(new AphrontRedirectResponse()); switch ($next_workflow) { case 'day': if (!$uri_query) { $uri_query = 'month'; } $year = $start_value->getDateTime()->format('Y'); $month = $start_value->getDateTime()->format('m'); $day = $start_value->getDateTime()->format('d'); $response->setURI( '/calendar/query/'.$uri_query.'/'.$year.'/'.$month.'/'.$day.'/'); break; default: $response->setURI('/E'.$event->getID()); break; } return $response; } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $error_name = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_NAME); $error_start_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_START_DATE); $error_end_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_END_DATE); $error_recurrence_end_date = $ex->getShortMessage( PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE); } } $is_recurring_checkbox = null; $recurrence_end_date_control = null; $recurrence_frequency_select = null; $all_day_checkbox = null; $start_control = null; $end_control = null; $recurring_date_edit_label = null; $current_policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($event) ->execute(); $name = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($name) ->setError($error_name); if ($this->isCreate()) { Javelin::initBehavior('recurring-edit', array( 'isRecurring' => $is_recurring_id, 'frequency' => $frequency_id, 'recurrenceEndDate' => $recurrence_end_date_id, )); $is_recurring_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isRecurring', 1, pht('Recurring Event'), $is_recurring, $is_recurring_id); $recurrence_end_date_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('recurrenceEndDate') ->setLabel(pht('Recurrence End Date')) ->setError($error_recurrence_end_date) ->setValue($recurrence_end_date_value) ->setID($recurrence_end_date_id) ->setIsTimeDisabled(true) ->setIsDisabled($recurrence_end_date_value->isDisabled()) ->setAllowNull(true); $recurrence_frequency_select = id(new AphrontFormSelectControl()) ->setName('frequency') ->setOptions(array( PhabricatorCalendarEvent::FREQUENCY_DAILY => pht('Daily'), PhabricatorCalendarEvent::FREQUENCY_WEEKLY => pht('Weekly'), PhabricatorCalendarEvent::FREQUENCY_MONTHLY => pht('Monthly'), PhabricatorCalendarEvent::FREQUENCY_YEARLY => pht('Yearly'), )) ->setValue($frequency) ->setLabel(pht('Recurring Event Frequency')) ->setID($frequency_id) ->setDisabled(!$is_recurring); } if ($this->isCreate() || (!$is_parent && !$this->isCreate())) { Javelin::initBehavior('event-all-day', array( 'allDayID' => $all_day_id, 'startDateID' => $start_date_id, 'endDateID' => $end_date_id, )); $all_day_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isAllDay', 1, pht('All Day Event'), $is_all_day, $all_day_id); $start_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('start') ->setLabel(pht('Start')) ->setError($error_start_date) ->setValue($start_value) ->setID($start_date_id) ->setIsTimeDisabled($is_all_day) ->setEndDateID($end_date_id); $end_control = id(new AphrontFormDateControl()) ->setUser($viewer) ->setName('end') ->setLabel(pht('End')) ->setError($error_end_date) ->setValue($end_value) ->setID($end_date_id) ->setIsTimeDisabled($is_all_day); } else if ($is_parent) { $recurring_date_edit_label = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setValue(pht('Date and time of recurring event cannot be edited.')); if (!$recurrence_end_date_value->isDisabled()) { $disabled_recurrence_end_date_value = $recurrence_end_date_value->getValueAsFormat('M d, Y'); $recurrence_end_date_control = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setLabel(pht('Recurrence End Date')) ->setValue($disabled_recurrence_end_date_value) ->setDisabled(true); } $recurrence_frequency_select = id(new AphrontFormSelectControl()) ->setName('frequency') ->setOptions(array( 'daily' => pht('Daily'), 'weekly' => pht('Weekly'), 'monthly' => pht('Monthly'), 'yearly' => pht('Yearly'), )) ->setValue($frequency) ->setLabel(pht('Recurring Event Frequency')) ->setID($frequency_id) ->setDisabled(true); $all_day_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isAllDay', 1, pht('All Day Event'), $is_all_day, $all_day_id) ->setDisabled(true); $start_disabled = $start_value->getValueAsFormat('M d, Y, g:i A'); $end_disabled = $end_value->getValueAsFormat('M d, Y, g:i A'); $start_control = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setLabel(pht('Start')) ->setValue($start_disabled) ->setDisabled(true); $end_control = id(new AphrontFormStaticControl()) ->setUser($viewer) ->setLabel(pht('End')) ->setValue($end_disabled); } $projects = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($projects) ->setUser($viewer) ->setDatasource(new PhabricatorProjectDatasource()); $description = id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($description) ->setUser($viewer); $view_policies = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setValue($view_policy) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($event) ->setPolicies($current_policies) ->setSpacePHID($space) ->setName('viewPolicy'); $edit_policies = id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setValue($edit_policy) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($event) ->setPolicies($current_policies) ->setName('editPolicy'); $subscribers = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('subscribers') ->setValue($subscribers) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); $invitees = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Invitees')) ->setName('invitees') ->setValue($invitees) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()); if ($this->isCreate()) { $icon_uri = $this->getApplicationURI('icon/'); } else { $icon_uri = $this->getApplicationURI('icon/'.$event->getID().'/'); } $icon_display = PhabricatorCalendarIcon::renderIconForChooser($icon); $icon = id(new AphrontFormChooseButtonControl()) ->setLabel(pht('Icon')) ->setName('icon') ->setDisplayValue($icon_display) ->setButtonText(pht('Choose Icon...')) ->setChooseURI($icon_uri) ->setValue($icon); $form = id(new AphrontFormView()) ->addHiddenInput('next', $next_workflow) ->addHiddenInput('query', $uri_query) ->setUser($viewer) ->appendChild($name); if ($recurring_date_edit_label) { $form->appendControl($recurring_date_edit_label); } if ($is_recurring_checkbox) { $form->appendChild($is_recurring_checkbox); } if ($recurrence_end_date_control) { $form->appendChild($recurrence_end_date_control); } if ($recurrence_frequency_select) { $form->appendControl($recurrence_frequency_select); } $form ->appendChild($all_day_checkbox) ->appendChild($start_control) ->appendChild($end_control) ->appendControl($view_policies) ->appendControl($edit_policies) ->appendControl($subscribers) ->appendControl($invitees) ->appendChild($projects) ->appendChild($description) ->appendChild($icon); if ($request->isAjax()) { return $this->newDialog() ->setTitle($page_title) ->setWidth(AphrontDialogView::WIDTH_FULL) ->appendForm($form) ->addCancelButton($cancel_uri) ->addSubmitButton($submit_label); } $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_label); $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$this->isCreate()) { $crumbs->addTextCrumb('E'.$event->getId(), '/E'.$event->getId()); } $crumbs->addTextCrumb($page_title); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setValidationException($validation_exception) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $page_title, )); } public function getNewInviteeList(array $phids, $event) { $invitees = $event->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited_status = PhabricatorCalendarEventInvitee::STATUS_INVITED; $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $phids = array_fuse($phids); $new = array(); foreach ($phids as $phid) { $old_status = $event->getUserInviteStatus($phid); if ($old_status != $uninvited_status) { continue; } $new[$phid] = $invited_status; } foreach ($invitees as $invitee) { $deleted_invitee = !idx($phids, $invitee->getInviteePHID()); if ($deleted_invitee) { $new[$invitee->getInviteePHID()] = $uninvited_status; } } return $new; } private function getDefaultTimeValues($viewer) { $start = new DateTime('@'.time()); $start->setTimeZone($viewer->getTimeZone()); $start->setTime($start->format('H'), 0, 0); $start->modify('+1 hour'); $end = id(clone $start)->modify('+1 hour'); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $start->format('U')); $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $end->format('U')); return array($start_value, $end_value); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditIconController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditIconController.php index a8c9af97fb..e93baa52bd 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditIconController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditIconController.php @@ -1,102 +1,97 @@ id = idx($data, 'id'); - } - public function handleRequest(AphrontRequest $request) { $viewer = $request->getUser(); + $id = $request->getURIData('id'); - if ($this->id) { + if ($id) { $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$event) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI('/E'.$event->getID()); $event_icon = $event->getIcon(); } else { $cancel_uri = '/calendar/'; $event_icon = $request->getStr('value'); } require_celerity_resource('calendar-icon-css'); Javelin::initBehavior('phabricator-tooltips'); $calendar_icons = PhabricatorCalendarIcon::getIconMap(); if ($request->isFormPost()) { $v_icon = $request->getStr('icon'); return id(new AphrontAjaxResponse())->setContent( array( 'value' => $v_icon, 'display' => PhabricatorCalendarIcon::renderIconForChooser($v_icon), )); } $ii = 0; $buttons = array(); foreach ($calendar_icons as $icon => $label) { $view = id(new PHUIIconView()) ->setIconFont($icon); $aural = javelin_tag( 'span', array( 'aural' => true, ), pht('Choose "%s" Icon', $label)); if ($icon == $event_icon) { $class_extra = ' selected'; } else { $class_extra = null; } $buttons[] = javelin_tag( 'button', array( 'class' => 'icon-button'.$class_extra, 'name' => 'icon', 'value' => $icon, 'type' => 'submit', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $label, ), ), array( $aural, $view, )); if ((++$ii % 4) == 0) { $buttons[] = phutil_tag('br'); } } $buttons = phutil_tag( 'div', array( 'class' => 'icon-grid', ), $buttons); return $this->newDialog() ->setTitle(pht('Choose Calendar Event Icon')) ->appendChild($buttons) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php index 60cde4f561..a7f41c4a0c 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php @@ -1,92 +1,90 @@ id = $request->getURIData('id'); + $id = $request->getURIData('id'); $action = $request->getURIData('action'); $request = $this->getRequest(); $viewer = $request->getViewer(); $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$event) { return new Aphront404Response(); } $cancel_uri = '/E'.$event->getID(); $validation_exception = null; $is_attending = $event->getIsUserAttending($viewer->getPHID()); if ($request->isFormPost()) { $new_status = null; switch ($action) { case self::ACTION_ACCEPT: $new_status = $attending_status; break; case self::ACTION_JOIN: if ($is_attending) { $new_status = $declined_status; } else { $new_status = $attending_status; } break; case self::ACTION_DECLINE: $new_status = $declined_status; break; } $new_status = array($viewer->getPHID() => $new_status); $xaction = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType(PhabricatorCalendarEventTransaction::TYPE_INVITE) ->setNewValue($new_status); $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); try { $editor->applyTransactions($event, array($xaction)); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } if (($action == self::ACTION_JOIN && !$is_attending) || $action == self::ACTION_ACCEPT) { $title = pht('Join Event'); $paragraph = pht('Would you like to join this event?'); $submit = pht('Join'); } else { $title = pht('Decline Event'); $paragraph = pht('Would you like to decline this event?'); $submit = pht('Decline'); } return $this->newDialog() ->setTitle($title) ->setValidationException($validation_exception) ->appendParagraph($paragraph) ->addCancelButton($cancel_uri) ->addSubmitButton($submit); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 580e5ea5ee..a0d5be4e4d 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -1,391 +1,385 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $sequence = $request->getURIData('sequence'); $timeline = null; $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$event) { return new Aphront404Response(); } if ($sequence) { $result = $this->getEventAtIndexForGhostPHID( $viewer, $event->getPHID(), $sequence); if ($result) { $parent_event = $event; $event = $result; $event->attachParentEvent($parent_event); return id(new AphrontRedirectResponse()) ->setURI('/E'.$result->getID()); } else if ($sequence && $event->getIsRecurring()) { $parent_event = $event; $event = $event->generateNthGhost($sequence, $viewer); $event->attachParentEvent($parent_event); } else if ($sequence) { return new Aphront404Response(); } $title = $event->getMonogram().' ('.$sequence.')'; $page_title = $title.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, '/'.$event->getMonogram().'/'.$sequence); } else { $title = 'E'.$event->getID(); $page_title = $title.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, '/E'.$event->getID()); } if (!$event->getIsGhostEvent()) { $timeline = $this->buildTransactionTimeline( $event, new PhabricatorCalendarEventTransactionQuery()); } $header = $this->buildHeaderView($event); $actions = $this->buildActionView($event); $properties = $this->buildPropertyView($event); $properties->setActionList($actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Add To Plate'); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $event->getPHID()); if ($sequence) { $comment_uri = $this->getApplicationURI( '/event/comment/'.$event->getID().'/'.$sequence.'/'); } else { $comment_uri = $this->getApplicationURI( '/event/comment/'.$event->getID().'/'); } $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($event->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($comment_uri) ->setSubmitButtonName(pht('Add Comment')); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, $add_comment_form, ), array( 'title' => $page_title, 'pageObjects' => array($event->getPHID()), )); } private function buildHeaderView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_cancelled = $event->getIsCancelled(); $icon = $is_cancelled ? ('fa-times') : ('fa-calendar'); $color = $is_cancelled ? ('grey') : ('green'); $status = $is_cancelled ? pht('Cancelled') : pht('Active'); $invite_status = $event->getUserInviteStatus($viewer->getPHID()); $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $is_invite_pending = ($invite_status == $status_invited); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($event->getName()) ->setStatus($icon, $color, $status) ->setPolicyObject($event); if ($is_invite_pending) { $decline_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon(id(new PHUIIconView()) ->setIconFont('fa-times grey')) ->setHref($this->getApplicationURI("/event/decline/{$id}/")) ->setWorkflow(true) ->setText(pht('Decline')); $accept_button = id(new PHUIButtonView()) ->setTag('a') ->setIcon(id(new PHUIIconView()) ->setIconFont('fa-check green')) ->setHref($this->getApplicationURI("/event/accept/{$id}/")) ->setWorkflow(true) ->setText(pht('Accept')); $header->addActionLink($decline_button) ->addActionLink($accept_button); } return $header; } private function buildActionView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_cancelled = $event->getIsCancelled(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); $actions = id(new PhabricatorActionListView()) ->setObjectURI($this->getApplicationURI('event/'.$id.'/')) ->setUser($viewer) ->setObject($event); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, PhabricatorPolicyCapability::CAN_EDIT); $edit_label = false; $edit_uri = false; if ($event->getIsGhostEvent()) { $index = $event->getSequenceIndex(); $edit_label = pht('Edit This Instance'); $edit_uri = "event/edit/{$id}/{$index}/"; } else if ($event->getIsRecurrenceException()) { $edit_label = pht('Edit This Instance'); $edit_uri = "event/edit/{$id}/"; } else { $edit_label = pht('Edit'); $edit_uri = "event/edit/{$id}/"; } if ($edit_label && $edit_uri) { $actions->addAction( id(new PhabricatorActionView()) ->setName($edit_label) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI($edit_uri)) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); } if ($is_attending) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Join Event')) ->setIcon('fa-user-plus') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); if ($event->getIsGhostEvent()) { $index = $event->getSequenceIndex(); $can_reinstate = $event->getIsParentCancelled(); $cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate This Instance'); $cancel_disabled = (!$can_edit || $can_reinstate); $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/{$index}/"); } else if ($event->getIsRecurrenceException()) { $can_reinstate = $event->getIsParentCancelled(); $cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate This Instance'); $cancel_disabled = (!$can_edit || $can_reinstate); } else if ($event->getIsRecurrenceParent()) { $cancel_label = pht('Cancel Recurrence'); $reinstate_label = pht('Reinstate Recurrence'); $cancel_disabled = !$can_edit; } else { $cancel_label = pht('Cancel Event'); $reinstate_label = pht('Reinstate Event'); $cancel_disabled = !$can_edit; } if ($is_cancelled) { $actions->addAction( id(new PhabricatorActionView()) ->setName($reinstate_label) ->setIcon('fa-plus') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName($cancel_label) ->setIcon('fa-times') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } return $actions; } private function buildPropertyView(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($event); if ($event->getIsAllDay()) { $date_start = phabricator_date($event->getDateFrom(), $viewer); $date_end = phabricator_date($event->getDateTo(), $viewer); if ($date_start == $date_end) { $properties->addProperty( pht('Time'), phabricator_date($event->getDateFrom(), $viewer)); } else { $properties->addProperty( pht('Starts'), phabricator_date($event->getDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), phabricator_date($event->getDateTo(), $viewer)); } } else { $properties->addProperty( pht('Starts'), phabricator_datetime($event->getDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), phabricator_datetime($event->getDateTo(), $viewer)); } if ($event->getIsRecurring()) { $properties->addProperty( pht('Recurs'), ucwords(idx($event->getRecurrenceFrequency(), 'rule'))); if ($event->getRecurrenceEndDate()) { $properties->addProperty( pht('Recurrence Ends'), phabricator_datetime($event->getRecurrenceEndDate(), $viewer)); } if ($event->getInstanceOfEventPHID()) { $properties->addProperty( pht('Recurrence of Event'), pht('%s of %s', $event->getSequenceIndex(), $viewer->renderHandle($event->getInstanceOfEventPHID())->render())); } } $properties->addProperty( pht('Host'), $viewer->renderHandle($event->getUserPHID())); $invitees = $event->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } if ($invitees) { $invitee_list = new PHUIStatusListView(); $icon_invited = PHUIStatusItemView::ICON_OPEN; $icon_attending = PHUIStatusItemView::ICON_ACCEPT; $icon_declined = PHUIStatusItemView::ICON_REJECT; $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $icon_map = array( $status_invited => $icon_invited, $status_attending => $icon_attending, $status_declined => $icon_declined, ); $icon_color_map = array( $status_invited => null, $status_attending => 'green', $status_declined => 'red', ); foreach ($invitees as $invitee) { $item = new PHUIStatusItemView(); $invitee_phid = $invitee->getInviteePHID(); $status = $invitee->getStatus(); $target = $viewer->renderHandle($invitee_phid); $icon = $icon_map[$status]; $icon_color = $icon_color_map[$status]; $item->setIcon($icon, $icon_color) ->setTarget($target); $invitee_list->addItem($item); } } else { $invitee_list = phutil_tag( 'em', array(), pht('None')); } $properties->addProperty( pht('Invitees'), $invitee_list); $properties->invokeWillRenderEvent(); $icon_display = PhabricatorCalendarIcon::renderIconForChooser( $event->getIcon()); $properties->addProperty( pht('Icon'), $icon_display); if (strlen($event->getDescription())) { $description = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($event->getDescription()), 'default', $viewer); $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent($description); } return $properties; } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 4d2193f2bb..8614e68f56 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -1,564 +1,575 @@ setLabel(pht('Created By')) ->setKey('creatorPHIDs') ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Invited')) ->setKey('invitedPHIDs') ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()), id(new PhabricatorSearchDateControlField()) ->setLabel(pht('Occurs After')) ->setKey('rangeStart'), id(new PhabricatorSearchDateControlField()) ->setLabel(pht('Occurs Before')) ->setKey('rangeEnd') ->setAliases(array('rangeEnd')), id(new PhabricatorSearchCheckboxesField()) ->setKey('upcoming') ->setOptions(array( 'upcoming' => pht('Show only upcoming events.'), )), id(new PhabricatorSearchSelectField()) ->setLabel(pht('Cancelled Events')) ->setKey('isCancelled') ->setOptions($this->getCancelledOptions()) ->setDefault('active'), id(new PhabricatorSearchSelectField()) ->setLabel(pht('Display Options')) ->setKey('display') ->setOptions($this->getViewOptions()) ->setDefault('month'), ); } private function getCancelledOptions() { return array( 'active' => pht('Active Events Only'), 'cancelled' => pht('Cancelled Events Only'), 'both' => pht('Both Cancelled and Active Events'), ); } private function getViewOptions() { return array( 'month' => pht('Month View'), 'day' => pht('Day View'), 'list' => pht('List View'), ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); $viewer = $this->requireViewer(); if ($map['creatorPHIDs']) { $query->withCreatorPHIDs($map['creatorPHIDs']); } if ($map['invitedPHIDs']) { $query->withInvitedPHIDs($map['invitedPHIDs']); } $range_start = $map['rangeStart']; $range_end = $map['rangeEnd']; $display = $map['display']; if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') { $upcoming = true; } else { $upcoming = false; } list($range_start, $range_end) = $this->getQueryDateRange( $range_start, $range_end, $display, $upcoming); $query->withDateRange($range_start, $range_end); switch ($map['isCancelled']) { case 'active': $query->withIsCancelled(false); break; case 'cancelled': $query->withIsCancelled(true); break; } return $query->setGenerateGhosts(true); } private function getQueryDateRange( $start_date_wild, $end_date_wild, $display, $upcoming) { $start_date_value = $this->getSafeDate($start_date_wild); $end_date_value = $this->getSafeDate($end_date_wild); $viewer = $this->requireViewer(); $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); $min_range = null; $max_range = null; $min_range = $start_date_value->getEpoch(); $max_range = $end_date_value->getEpoch(); if ($display == 'month' || $display == 'day') { list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display); $start_day = new DateTime( "{$start_year}-{$start_month}-{$start_day}", $timezone); $next = clone $start_day; if ($display == 'month') { $next->modify('+1 month'); } else if ($display == 'day') { $next->modify('+7 day'); } $display_start = $start_day->format('U'); $display_end = $next->format('U'); $preferences = $viewer->loadPreferences(); $pref_week_day = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $start_of_week = $preferences->getPreference($pref_week_day, 0); $end_of_week = ($start_of_week + 6) % 7; $first_of_month = $start_day->format('w'); $last_of_month = id(clone $next)->modify('-1 day')->format('w'); if (!$min_range || ($min_range < $display_start)) { $min_range = $display_start; if ($display == 'month' && $first_of_month !== $start_of_week) { $interim_day_num = ($first_of_month + 7 - $start_of_week) % 7; $min_range = id(clone $start_day) ->modify('-'.$interim_day_num.' days') ->format('U'); } } if (!$max_range || ($max_range > $display_end)) { $max_range = $display_end; if ($display == 'month' && $last_of_month !== $end_of_week) { $interim_day_num = ($end_of_week + 7 - $last_of_month) % 7; $max_range = id(clone $next) ->modify('+'.$interim_day_num.' days') ->format('U'); } } } if ($upcoming) { if ($min_range) { $min_range = max(time(), $min_range); } else { $min_range = time(); } } return array($min_range, $max_range); } protected function getURI($path) { return '/calendar/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'month' => pht('Month View'), 'day' => pht('Day View'), 'upcoming' => pht('Upcoming Events'), 'all' => pht('All Events'), ); return $names; } public function setCalendarYearAndMonthAndDay($year, $month, $day = null) { $this->calendarYear = $year; $this->calendarMonth = $month; $this->calendarDay = $day; return $this; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'month': return $query->setParameter('display', 'month'); case 'day': return $query->setParameter('display', 'day'); case 'upcoming': return $query ->setParameter('display', 'list') ->setParameter('upcoming', array( 0 => 'upcoming', )); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { $phids = array(); foreach ($objects as $event) { $phids[$event->getUserPHID()] = 1; } return array_keys($phids); } protected function renderResultList( array $events, PhabricatorSavedQuery $query, array $handles) { if ($this->isMonthView($query)) { return $this->buildCalendarView($events, $query, $handles); } else if ($this->isDayView($query)) { return $this->buildCalendarDayView($events, $query, $handles); } assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($events as $event) { $duration = ''; $event_date_info = $this->getEventDateLabel($event); $creator_handle = $handles[$event->getUserPHID()]; $attendees = array(); foreach ($event->getInvitees() as $invitee) { $attendees[] = $invitee->getInviteePHID(); } $attendees = pht( 'Attending: %s', $viewer->renderHandleList($attendees) ->setAsInline(1) ->render()); if (strlen($event->getDuration()) > 0) { $duration = pht( 'Duration: %s', $event->getDuration()); } + if ($event->getIsGhostEvent()) { + $title_text = $event->getMonogram() + .' (' + .$event->getSequenceIndex() + .'): ' + .$event->getName(); + } else { + $title_text = $event->getMonogram().': '.$event->getName(); + } + $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setObject($event) - ->setHeader($viewer->renderHandle($event->getPHID())->render()) + ->setHeader($title_text) + ->setHref($event->getURI()) ->addAttribute($event_date_info) ->addAttribute($attendees) ->addIcon('none', $duration); $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No events found.')); return $result; } private function buildCalendarView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); $now = time(); list($start_year, $start_month) = $this->getDisplayYearAndMonthAndDay( $this->getQueryDateFrom($query)->getEpoch(), $this->getQueryDateTo($query)->getEpoch(), $query->getParameter('display')); $now_year = phabricator_format_local_time($now, $viewer, 'Y'); $now_month = phabricator_format_local_time($now, $viewer, 'm'); $now_day = phabricator_format_local_time($now, $viewer, 'j'); if ($start_month == $now_month && $start_year == $now_year) { $month_view = new PHUICalendarMonthView( $this->getQueryDateFrom($query), $this->getQueryDateTo($query), $start_month, $start_year, $now_day); } else { $month_view = new PHUICalendarMonthView( $this->getQueryDateFrom($query), $this->getQueryDateTo($query), $start_month, $start_year); } $month_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setIcon($status->getIcon()); $name_text = $handles[$status->getUserPHID()]->getName(); $status_text = $status->getName(); $event->setUserPHID($status->getUserPHID()); $event->setDescription(pht('%s (%s)', $name_text, $status_text)); $event->setName($status_text); $event->setURI($status->getURI()); $event->setViewerIsInvited($viewer_is_invited); $month_view->addEvent($event); } $month_view->setBrowseURI( $this->getURI('query/'.$query->getQueryKey().'/')); // TODO redesign-2015 : Move buttons out of PHUICalendarView? $result = new PhabricatorApplicationSearchResultView(); $result->setContent($month_view); return $result; } private function buildCalendarDayView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay( $this->getQueryDateFrom($query)->getEpoch(), $this->getQueryDateTo($query)->getEpoch(), $query->getParameter('display')); $day_view = id(new PHUICalendarDayView( $this->getQueryDateFrom($query)->getEpoch(), $this->getQueryDateTo($query)->getEpoch(), $start_year, $start_month, $start_day)) ->setQuery($query->getQueryKey()); $day_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { if ($status->getIsCancelled()) { continue; } $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $status, PhabricatorPolicyCapability::CAN_EDIT); $event = new AphrontCalendarEventView(); $event->setCanEdit($can_edit); $event->setEventID($status->getID()); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setIcon($status->getIcon()); $event->setViewerIsInvited($viewer_is_invited); $event->setName($status->getName()); $event->setURI($status->getURI()); $day_view->addEvent($event); } $day_view->setBrowseURI( $this->getURI('query/'.$query->getQueryKey().'/')); $result = new PhabricatorApplicationSearchResultView(); $result->setContent($day_view); return $result; } private function getDisplayYearAndMonthAndDay( $range_start, $range_end, $display) { $viewer = $this->requireViewer(); $epoch = null; if ($this->calendarYear && $this->calendarMonth) { $start_year = $this->calendarYear; $start_month = $this->calendarMonth; $start_day = $this->calendarDay ? $this->calendarDay : 1; } else { if ($range_start) { $epoch = $range_start; } else if ($range_end) { $epoch = $range_end; } else { $epoch = time(); } if ($display == 'month') { $day = 1; } else { $day = phabricator_format_local_time($epoch, $viewer, 'd'); } $start_year = phabricator_format_local_time($epoch, $viewer, 'Y'); $start_month = phabricator_format_local_time($epoch, $viewer, 'm'); $start_day = $day; } return array($start_year, $start_month, $start_day); } public function getPageSize(PhabricatorSavedQuery $saved) { if ($this->isMonthView($saved) || $this->isDayView($saved)) { return $saved->getParameter('limit', 1000); } else { return $saved->getParameter('limit', 100); } } private function getQueryDateFrom(PhabricatorSavedQuery $saved) { return $this->getQueryDate($saved, 'rangeStart'); } private function getQueryDateTo(PhabricatorSavedQuery $saved) { return $this->getQueryDate($saved, 'rangeEnd'); } private function getQueryDate(PhabricatorSavedQuery $saved, $key) { $viewer = $this->requireViewer(); $wild = $saved->getParameter($key); return $this->getSafeDate($wild); } private function getSafeDate($value) { $viewer = $this->requireViewer(); if ($value) { // ideally this would be consistent and always pass in the same type if ($value instanceof AphrontFormDateControlValue) { return $value; } else { $value = AphrontFormDateControlValue::newFromWild($viewer, $value); } } else { $value = AphrontFormDateControlValue::newFromEpoch( $viewer, PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U')); $value->setEnabled(false); } $value->setOptional(true); return $value; } private function isMonthView(PhabricatorSavedQuery $query) { if ($this->isDayView($query)) { return false; } if ($query->getParameter('display') == 'month') { return true; } } private function isDayView(PhabricatorSavedQuery $query) { if ($query->getParameter('display') == 'day') { return true; } if ($this->calendarDay) { return true; } return false; } private function getEventDateLabel($event) { $viewer = $this->requireViewer(); $from_datetime = PhabricatorTime::getDateTimeFromEpoch( $event->getDateFrom(), $viewer); $to_datetime = PhabricatorTime::getDateTimeFromEpoch( $event->getDateTo(), $viewer); $from_date_formatted = $from_datetime->format('Y m d'); $to_date_formatted = $to_datetime->format('Y m d'); if ($event->getIsAllDay()) { if ($from_date_formatted == $to_date_formatted) { return pht( '%s, All Day', phabricator_date($event->getDateFrom(), $viewer)); } else { return pht( '%s - %s, All Day', phabricator_date($event->getDateFrom(), $viewer), phabricator_date($event->getDateTo(), $viewer)); } } else if ($from_date_formatted == $to_date_formatted) { return pht( '%s - %s', phabricator_datetime($event->getDateFrom(), $viewer), phabricator_time($event->getDateTo(), $viewer)); } else { return pht( '%s - %s', phabricator_datetime($event->getDateFrom(), $viewer), phabricator_datetime($event->getDateTo(), $viewer)); } } } diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php index a7c5c68153..cdff189633 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php @@ -1,45 +1,44 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $channels = id(new PhabricatorChatLogChannelQuery()) - ->setViewer($user) + ->setViewer($viewer) ->execute(); $list = new PHUIObjectItemListView(); foreach ($channels as $channel) { $item = id(new PHUIObjectItemView()) ->setHeader($channel->getChannelName()) ->setHref('/chatlog/channel/'.$channel->getID().'/') ->addAttribute($channel->getServiceName()) ->addAttribute($channel->getServiceType()); $list->addItem($item); } $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Channel List'), $this->getApplicationURI()); $box = id(new PHUIObjectBoxView()) ->setHeaderText('Channel List') ->setObjectList($list); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => pht('Channel List'), )); } } diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php index 4281adf421..8084356ced 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php @@ -1,330 +1,324 @@ channelID = $data['channelID']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('channelID'); $uri = clone $request->getRequestURI(); $uri->setQueryParams(array()); $pager = new AphrontCursorPagerView(); $pager->setURI($uri); $pager->setPageSize(250); $query = id(new PhabricatorChatLogQuery()) - ->setViewer($user) - ->withChannelIDs(array($this->channelID)); + ->setViewer($viewer) + ->withChannelIDs(array($id)); $channel = id(new PhabricatorChatLogChannelQuery()) - ->setViewer($user) - ->withIDs(array($this->channelID)) + ->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, $user); + $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($user) + ->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')) ->setIconFont('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->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => pht('Channel Log'), )); } /** * 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) { - $user = $request->getUser(); + $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, $user); + $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/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index 4472f80864..39fa9b66f4 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,383 +1,406 @@ 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(); + abstract protected function defineParamTypes(); abstract protected function defineReturnType(); protected function defineErrorTypes() { return array(); } abstract protected function execute(ConduitAPIRequest $request); public function __construct() {} 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() { // by default, conduit methods are not accessible via OAuth return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; } public function executeMethod(ConduitAPIRequest $request) { 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 function getApplicationName() { return head(explode('.', $this->getAPIMethodName(), 2)); } public static function loadAllConduitMethods() { static $method_map = null; if ($method_map === null) { $methods = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); foreach ($methods as $method) { $name = $method->getAPIMethodName(); if (empty($method_map[$name])) { $method_map[$name] = $method; continue; } $orig_class = get_class($method_map[$name]); $this_class = get_class($method); throw new Exception( pht( 'Two Conduit API method classes (%s, %s) both have the same '. 'method name (%s). API methods must have unique method names.', $orig_class, $this_class, $name)); } } return $method_map; } public static function getConduitMethod($method_name) { $method_map = self::loadAllConduitMethods(); return idx($method_map, $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; } /* -( 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; } public function describeAutomaticCapability($capability) { return null; } 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); } } diff --git a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php index 5e12cf7f99..6c067a8ff4 100644 --- a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php +++ b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php @@ -1,205 +1,205 @@ setParameter('isStable', $request->getStr('isStable')); $saved->setParameter('isUnstable', $request->getStr('isUnstable')); $saved->setParameter('isDeprecated', $request->getStr('isDeprecated')); $saved->setParameter( 'applicationNames', $request->getStrList('applicationNames')); $saved->setParameter('nameContains', $request->getStr('nameContains')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorConduitMethodQuery()); $query->withIsStable($saved->getParameter('isStable')); $query->withIsUnstable($saved->getParameter('isUnstable')); $query->withIsDeprecated($saved->getParameter('isDeprecated')); $names = $saved->getParameter('applicationNames', array()); if ($names) { $query->withApplicationNames($names); } $contains = $saved->getParameter('nameContains'); if (strlen($contains)) { $query->withNameContains($contains); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name Contains')) ->setName('nameContains') ->setValue($saved->getParameter('nameContains'))); $names = $saved->getParameter('applicationNames', array()); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Applications')) ->setName('applicationNames') ->setValue(implode(', ', $names)) ->setCaption( pht( 'Example: %s', phutil_tag('tt', array(), 'differential, paste')))); $is_stable = $saved->getParameter('isStable'); $is_unstable = $saved->getParameter('isUnstable'); $is_deprecated = $saved->getParameter('isDeprecated'); $form ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel('Stability') ->addCheckbox( 'isStable', 1, hsprintf( '%s: %s', pht('Stable Methods'), pht('Show established API methods with stable interfaces.')), $is_stable) ->addCheckbox( 'isUnstable', 1, hsprintf( '%s: %s', pht('Unstable Methods'), pht('Show new methods which are subject to change.')), $is_unstable) ->addCheckbox( 'isDeprecated', 1, hsprintf( '%s: %s', pht('Deprecated Methods'), pht( 'Show old methods which will be deleted in a future '. 'version of Phabricator.')), $is_deprecated)); } protected function getURI($path) { return '/conduit/'.$path; } protected function getBuiltinQueryNames() { return array( 'modern' => pht('Modern Methods'), 'all' => pht('All Methods'), ); } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'modern': return $query ->setParameter('isStable', true) ->setParameter('isUnstable', true); case 'all': return $query ->setParameter('isStable', true) ->setParameter('isUnstable', true) ->setParameter('isDeprecated', true); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $methods, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($methods, 'ConduitAPIMethod'); $viewer = $this->requireViewer(); $out = array(); $last = null; $list = null; foreach ($methods as $method) { $app = $method->getApplicationName(); if ($app !== $last) { $last = $app; if ($list) { $out[] = $list; } $list = id(new PHUIObjectItemListView()); $list->setHeader($app); $app_object = $method->getApplication(); if ($app_object) { $app_name = $app_object->getName(); } else { $app_name = $app; } } $method_name = $method->getAPIMethodName(); $item = id(new PHUIObjectItemView()) ->setHeader($method_name) ->setHref($this->getApplicationURI('method/'.$method_name.'/')) - ->addAttribute($method->getMethodDescription()); + ->addAttribute($method->getMethodSummary()); switch ($method->getMethodStatus()) { case ConduitAPIMethod::METHOD_STATUS_STABLE: break; case ConduitAPIMethod::METHOD_STATUS_UNSTABLE: $item->addIcon('fa-warning', pht('Unstable')); $item->setStatusIcon('fa-warning yellow'); break; case ConduitAPIMethod::METHOD_STATUS_DEPRECATED: $item->addIcon('fa-warning', pht('Deprecated')); $item->setStatusIcon('fa-warning red'); break; } $list->addItem($item); } if ($list) { $out[] = $list; } $result = new PhabricatorApplicationSearchResultView(); $result->setContent($out); return $result; } } diff --git a/src/applications/config/controller/PhabricatorConfigAllController.php b/src/applications/config/controller/PhabricatorConfigAllController.php index 309304ed21..17bf65fd7b 100644 --- a/src/applications/config/controller/PhabricatorConfigAllController.php +++ b/src/applications/config/controller/PhabricatorConfigAllController.php @@ -1,135 +1,134 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $db_values = id(new PhabricatorConfigEntry()) ->loadAllWhere('namespace = %s', 'default'); $db_values = mpull($db_values, null, 'getConfigKey'); $rows = array(); $options = PhabricatorApplicationConfigOptions::loadAllOptions(); ksort($options); foreach ($options as $option) { $key = $option->getKey(); if ($option->getHidden()) { $value = phutil_tag('em', array(), pht('Hidden')); } else { $value = PhabricatorEnv::getEnvConfig($key); $value = PhabricatorConfigJSON::prettyPrintJSON($value); } $db_value = idx($db_values, $key); $rows[] = array( phutil_tag( 'a', array( 'href' => $this->getApplicationURI('edit/'.$key.'/'), ), $key), $value, $db_value && !$db_value->getIsDeleted() ? pht('Customized') : '', ); } $table = id(new AphrontTableView($rows)) ->setColumnClasses( array( '', 'wide', )) ->setHeaders( array( pht('Key'), pht('Value'), pht('Customized'), )); $title = pht('Current Settings'); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb($title); $panel = new PHUIObjectBoxView(); $panel->setHeaderText(pht('Current Settings')); $panel->setTable($table); $versions = $this->loadVersions(); $version_property_list = id(new PHUIPropertyListView()); foreach ($versions as $version) { list($name, $hash) = $version; $version_property_list->addProperty($name, $hash); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Current Version')) ->addPropertyList($version_property_list); $phabricator_root = dirname(phutil_get_library_root('phabricator')); $version_path = $phabricator_root.'/conf/local/VERSION'; if (Filesystem::pathExists($version_path)) { $version_from_file = Filesystem::readFile($version_path); $version_property_list->addProperty( pht('Local Version'), $version_from_file); } $nav = $this->buildSideNavView(); $nav->selectFilter('all/'); $nav->setCrumbs($crumbs); $nav->appendChild($object_box); $nav->appendChild($panel); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function loadVersions() { $specs = array( array( 'name' => pht('Phabricator Version'), 'root' => 'phabricator', ), array( 'name' => pht('Arcanist Version'), 'root' => 'arcanist', ), array( 'name' => pht('libphutil Version'), 'root' => 'phutil', ), ); $futures = array(); foreach ($specs as $key => $spec) { $root = dirname(phutil_get_library_root($spec['root'])); $futures[$key] = id(new ExecFuture('git log --format=%%H -n 1 --')) ->setCWD($root); } $results = array(); foreach ($futures as $key => $future) { list($err, $stdout) = $future->resolve(); if (!$err) { $name = trim($stdout); } else { $name = pht('Unknown'); } $results[$key] = array($specs[$key]['name'], $name); } return array_select_keys($results, array_keys($specs)); } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index 252d8a73de..22df5104e4 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -1,170 +1,169 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $query = $this->buildSchemaQuery(); $actual = $query->loadActualSchema(); $expect = $query->loadExpectedSchema(); $comp = $query->buildComparisonSchema($expect, $actual); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Database Issues')); // Collect all open issues. $issues = array(); foreach ($comp->getDatabases() as $database_name => $database) { foreach ($database->getLocalIssues() as $issue) { $issues[] = array( $database_name, null, null, null, $issue, ); } foreach ($database->getTables() as $table_name => $table) { foreach ($table->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, null, null, $issue, ); } foreach ($table->getColumns() as $column_name => $column) { foreach ($column->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, 'column', $column_name, $issue, ); } } foreach ($table->getKeys() as $key_name => $key) { foreach ($key->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, 'key', $key_name, $issue, ); } } } } // Sort all open issues so that the most severe issues appear first. $order = array(); $counts = array(); foreach ($issues as $key => $issue) { $const = $issue[4]; $status = PhabricatorConfigStorageSchema::getIssueStatus($const); $severity = PhabricatorConfigStorageSchema::getStatusSeverity($status); $order[$key] = sprintf( '~%d~%s%s%s', 9 - $severity, $issue[0], $issue[1], $issue[3]); if (empty($counts[$status])) { $counts[$status] = 0; } $counts[$status]++; } asort($order); $issues = array_select_keys($issues, array_keys($order)); // Render the issues. $rows = array(); foreach ($issues as $issue) { $const = $issue[4]; $database_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('/database/'.$issue[0].'/'), ), $issue[0]); $rows[] = array( $this->renderIcon( PhabricatorConfigStorageSchema::getIssueStatus($const)), $database_link, $issue[1], $issue[2], $issue[3], PhabricatorConfigStorageSchema::getIssueDescription($const), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Database'), pht('Table'), pht('Type'), pht('Column/Key'), pht('Issue'), )) ->setColumnClasses( array( null, null, null, null, null, 'wide', )); $errors = array(); if (isset($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])) { $errors[] = pht( 'Detected %s serious issue(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])); } if (isset($counts[PhabricatorConfigStorageSchema::STATUS_WARN])) { $errors[] = pht( 'Detected %s warning(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_WARN])); } $title = pht('Database Issues'); $table_box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderWithDocumentationLink($title)) ->setFormErrors($errors) ->setTable($table); $nav = $this->buildSideNavView(); $nav->selectFilter('dbissue/'); $nav->appendChild( array( $crumbs, $table_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php index f69e794fb8..d886e98d74 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php @@ -1,768 +1,764 @@ database = idx($data, 'database'); - $this->table = idx($data, 'table'); - $this->column = idx($data, 'column'); - $this->key = idx($data, 'key'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $this->database = $request->getURIData('database'); + $this->table = $request->getURIData('table'); + $this->column = $request->getURIData('column'); + $this->key = $request->getURIData('key'); $query = $this->buildSchemaQuery(); $actual = $query->loadActualSchema(); $expect = $query->loadExpectedSchema(); $comp = $query->buildComparisonSchema($expect, $actual); if ($this->column) { return $this->renderColumn( $comp, $expect, $actual, $this->database, $this->table, $this->column); } else if ($this->key) { return $this->renderKey( $comp, $expect, $actual, $this->database, $this->table, $this->key); } else if ($this->table) { return $this->renderTable( $comp, $expect, $actual, $this->database, $this->table); } else if ($this->database) { return $this->renderDatabase( $comp, $expect, $actual, $this->database); } else { return $this->renderServer( $comp, $expect, $actual); } } private function buildResponse($title, $body) { $nav = $this->buildSideNavView(); $nav->selectFilter('database/'); $crumbs = $this->buildApplicationCrumbs(); if ($this->database) { $crumbs->addTextCrumb( pht('Database Status'), $this->getApplicationURI('database/')); if ($this->table) { $crumbs->addTextCrumb( $this->database, $this->getApplicationURI('database/'.$this->database.'/')); if ($this->column || $this->key) { $crumbs->addTextCrumb( $this->table, $this->getApplicationURI( 'database/'.$this->database.'/'.$this->table.'/')); if ($this->column) { $crumbs->addTextCrumb($this->column); } else { $crumbs->addTextCrumb($this->key); } } else { $crumbs->addTextCrumb($this->table); } } else { $crumbs->addTextCrumb($this->database); } } else { $crumbs->addTextCrumb(pht('Database Status')); } $nav->setCrumbs($crumbs); $nav->appendChild($body); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function renderServer( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual) { $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $rows = array(); foreach ($comp->getDatabases() as $database_name => $database) { $actual_database = $actual->getDatabase($database_name); if ($actual_database) { $charset = $actual_database->getCharacterSet(); $collation = $actual_database->getCollation(); } else { $charset = null; $collation = null; } $status = $database->getStatus(); $issues = $database->getIssues(); $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( '/database/'.$database_name.'/'), ), $database_name), $this->renderAttr($charset, $database->hasIssue($charset_issue)), $this->renderAttr($collation, $database->hasIssue($collation_issue)), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Database'), pht('Charset'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, null, )); $title = pht('Database Status'); $properties = $this->buildProperties( array( ), $comp->getIssues()); $prop_box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderWithDocumentationLink($title)) ->addPropertyList($properties); $table_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Databases')) ->setTable($table); return $this->buildResponse($title, array($prop_box, $table_box)); } private function renderDatabase( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name) { $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $rows = array(); foreach ($database->getTables() as $table_name => $table) { $status = $table->getStatus(); $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( '/database/'.$database_name.'/'.$table_name.'/'), ), $table_name), $this->renderAttr( $table->getCollation(), $table->hasIssue($collation_issue)), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Table'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, )); $title = pht('Database: %s', $database_name); $actual_database = $actual->getDatabase($database_name); if ($actual_database) { $actual_charset = $actual_database->getCharacterSet(); $actual_collation = $actual_database->getCollation(); } else { $actual_charset = null; $actual_collation = null; } $expect_database = $expect->getDatabase($database_name); if ($expect_database) { $expect_charset = $expect_database->getCharacterSet(); $expect_collation = $expect_database->getCollation(); } else { $expect_charset = null; $expect_collation = null; } $properties = $this->buildProperties( array( array( pht('Character Set'), $actual_charset, ), array( pht('Expected Character Set'), $expect_charset, ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), ), $database->getIssues()); $prop_box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderWithDocumentationLink($title)) ->addPropertyList($properties); $table_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Database Status')) ->setTable($table); return $this->buildResponse($title, array($prop_box, $table_box)); } private function renderTable( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name) { $type_issue = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $nullable_issue = PhabricatorConfigStorageSchema::ISSUE_NULLABLE; $unique_issue = PhabricatorConfigStorageSchema::ISSUE_UNIQUE; $columns_issue = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS; $longkey_issue = PhabricatorConfigStorageSchema::ISSUE_LONGKEY; $auto_issue = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT; $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); } $expect_database = $expect->getDatabase($database_name); $expect_table = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); } $rows = array(); foreach ($table->getColumns() as $column_name => $column) { $expect_column = null; if ($expect_table) { $expect_column = $expect_table->getColumn($column_name); } $status = $column->getStatus(); $data_type = null; if ($expect_column) { $data_type = $expect_column->getDataType(); } $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( 'database/'. $database_name.'/'. $table_name.'/'. 'col/'. $column_name.'/'), ), $column_name), $data_type, $this->renderAttr( $column->getColumnType(), $column->hasIssue($type_issue)), $this->renderAttr( $this->renderBoolean($column->getNullable()), $column->hasIssue($nullable_issue)), $this->renderAttr( $this->renderBoolean($column->getAutoIncrement()), $column->hasIssue($auto_issue)), $this->renderAttr( $column->getCharacterSet(), $column->hasIssue($charset_issue)), $this->renderAttr( $column->getCollation(), $column->hasIssue($collation_issue)), ); } $table_view = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Column'), pht('Data Type'), pht('Column Type'), pht('Nullable'), pht('Autoincrement'), pht('Character Set'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, null, null, null, null, )); $key_rows = array(); foreach ($table->getKeys() as $key_name => $key) { $expect_key = null; if ($expect_table) { $expect_key = $expect_table->getKey($key_name); } $status = $key->getStatus(); $size = 0; foreach ($key->getColumnNames() as $column_spec) { list($column_name, $prefix) = $key->getKeyColumnAndPrefix($column_spec); $column = $table->getColumn($column_name); if (!$column) { $size = 0; break; } $size += $column->getKeyByteLength($prefix); } $size_formatted = null; if ($size) { $size_formatted = $this->renderAttr( $size, $key->hasIssue($longkey_issue)); } $key_rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( 'database/'. $database_name.'/'. $table_name.'/'. 'key/'. $key_name.'/'), ), $key_name), $this->renderAttr( implode(', ', $key->getColumnNames()), $key->hasIssue($columns_issue)), $this->renderAttr( $this->renderBoolean($key->getUnique()), $key->hasIssue($unique_issue)), $size_formatted, ); } $keys_view = id(new AphrontTableView($key_rows)) ->setHeaders( array( null, pht('Key'), pht('Columns'), pht('Unique'), pht('Size'), )) ->setColumnClasses( array( null, 'wide pri', null, null, null, )); $title = pht('Database: %s.%s', $database_name, $table_name); if ($actual_table) { $actual_collation = $actual_table->getCollation(); } else { $actual_collation = null; } if ($expect_table) { $expect_collation = $expect_table->getCollation(); } else { $expect_collation = null; } $properties = $this->buildProperties( array( array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), ), $table->getIssues()); $prop_box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderWithDocumentationLink($title)) ->addPropertyList($properties); $table_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Database')) ->setTable($table_view); $key_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Keys')) ->setTable($keys_view); return $this->buildResponse($title, array($prop_box, $table_box, $key_box)); } private function renderColumn( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name, $column_name) { $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $column = $table->getColumn($column_name); if (!$column) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; $actual_column = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); if ($actual_table) { $actual_column = $actual_table->getColumn($column_name); } } $expect_database = $expect->getDatabase($database_name); $expect_table = null; $expect_column = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $expect_column = $expect_table->getColumn($column_name); } } if ($actual_column) { $actual_coltype = $actual_column->getColumnType(); $actual_charset = $actual_column->getCharacterSet(); $actual_collation = $actual_column->getCollation(); $actual_nullable = $actual_column->getNullable(); $actual_auto = $actual_column->getAutoIncrement(); } else { $actual_coltype = null; $actual_charset = null; $actual_collation = null; $actual_nullable = null; $actual_auto = null; } if ($expect_column) { $data_type = $expect_column->getDataType(); $expect_coltype = $expect_column->getColumnType(); $expect_charset = $expect_column->getCharacterSet(); $expect_collation = $expect_column->getCollation(); $expect_nullable = $expect_column->getNullable(); $expect_auto = $expect_column->getAutoIncrement(); } else { $data_type = null; $expect_coltype = null; $expect_charset = null; $expect_collation = null; $expect_nullable = null; $expect_auto = null; } $title = pht( 'Database Status: %s.%s.%s', $database_name, $table_name, $column_name); $properties = $this->buildProperties( array( array( pht('Data Type'), $data_type, ), array( pht('Column Type'), $actual_coltype, ), array( pht('Expected Column Type'), $expect_coltype, ), array( pht('Character Set'), $actual_charset, ), array( pht('Expected Character Set'), $expect_charset, ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), array( pht('Nullable'), $this->renderBoolean($actual_nullable), ), array( pht('Expected Nullable'), $this->renderBoolean($expect_nullable), ), array( pht('Autoincrement'), $this->renderBoolean($actual_auto), ), array( pht('Expected Autoincrement'), $this->renderBoolean($expect_auto), ), ), $column->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderWithDocumentationLink($title)) ->addPropertyList($properties); return $this->buildResponse($title, $box); } private function renderKey( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name, $key_name) { $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $key = $table->getKey($key_name); if (!$key) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; $actual_key = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); if ($actual_table) { $actual_key = $actual_table->getKey($key_name); } } $expect_database = $expect->getDatabase($database_name); $expect_table = null; $expect_key = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $expect_key = $expect_table->getKey($key_name); } } if ($actual_key) { $actual_columns = $actual_key->getColumnNames(); $actual_unique = $actual_key->getUnique(); } else { $actual_columns = array(); $actual_unique = null; } if ($expect_key) { $expect_columns = $expect_key->getColumnNames(); $expect_unique = $expect_key->getUnique(); } else { $expect_columns = array(); $expect_unique = null; } $title = pht( 'Database Status: %s.%s (%s)', $database_name, $table_name, $key_name); $properties = $this->buildProperties( array( array( pht('Unique'), $this->renderBoolean($actual_unique), ), array( pht('Expected Unique'), $this->renderBoolean($expect_unique), ), array( pht('Columns'), implode(', ', $actual_columns), ), array( pht('Expected Columns'), implode(', ', $expect_columns), ), ), $key->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderWithDocumentationLink($title)) ->addPropertyList($properties); return $this->buildResponse($title, $box); } private function buildProperties(array $properties, array $issues) { $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()); foreach ($properties as $property) { list($key, $value) = $property; $view->addProperty($key, $value); } $status_view = new PHUIStatusListView(); if (!$issues) { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('No Schema Issues'))); } else { foreach ($issues as $issue) { $note = PhabricatorConfigStorageSchema::getIssueDescription($issue); $status = PhabricatorConfigStorageSchema::getIssueStatus($issue); switch ($status) { case PhabricatorConfigStorageSchema::STATUS_WARN: $icon = PHUIStatusItemView::ICON_WARNING; $color = 'yellow'; break; case PhabricatorConfigStorageSchema::STATUS_FAIL: default: $icon = PHUIStatusItemView::ICON_REJECT; $color = 'red'; break; } $item = id(new PHUIStatusItemView()) ->setTarget(PhabricatorConfigStorageSchema::getIssueName($issue)) ->setIcon($icon, $color) ->setNote($note); $status_view->addItem($item); } } $view->addProperty(pht('Schema Status'), $status_view); return $view; } } diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index f3360b3496..90547f41f7 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -1,541 +1,535 @@ key = $data['key']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $key = $request->getURIData('key'); $options = PhabricatorApplicationConfigOptions::loadAllOptions(); - if (empty($options[$this->key])) { + if (empty($options[$key])) { $ancient = PhabricatorExtraConfigSetupCheck::getAncientConfig(); - if (isset($ancient[$this->key])) { + if (isset($ancient[$key])) { $desc = pht( "This configuration has been removed. You can safely delete ". "it.\n\n%s", - $ancient[$this->key]); + $ancient[$key]); } else { $desc = pht( 'This configuration option is unknown. It may be misspelled, '. 'or have existed in a previous version of Phabricator.'); } // This may be a dead config entry, which existed in the past but no // longer exists. Allow it to be edited so it can be reviewed and // deleted. $option = id(new PhabricatorConfigOption()) - ->setKey($this->key) + ->setKey($key) ->setType('wild') ->setDefault(null) ->setDescription($desc); $group = null; $group_uri = $this->getApplicationURI(); } else { - $option = $options[$this->key]; + $option = $options[$key]; $group = $option->getGroup(); $group_uri = $this->getApplicationURI('group/'.$group->getKey().'/'); } $issue = $request->getStr('issue'); if ($issue) { // If the user came here from an open setup issue, send them back. $done_uri = $this->getApplicationURI('issue/'.$issue.'/'); } else { $done_uri = $group_uri; } // Check if the config key is already stored in the database. // Grab the value if it is. $config_entry = id(new PhabricatorConfigEntry()) ->loadOneWhere( 'configKey = %s AND namespace = %s', - $this->key, + $key, 'default'); if (!$config_entry) { $config_entry = id(new PhabricatorConfigEntry()) - ->setConfigKey($this->key) + ->setConfigKey($key) ->setNamespace('default') ->setIsDeleted(true); $config_entry->setPHID($config_entry->generatePHID()); } $e_value = null; $errors = array(); if ($request->isFormPost() && !$option->getLocked()) { $result = $this->readRequest( $option, $request); list($e_value, $value_errors, $display_value, $xaction) = $result; $errors = array_merge($errors, $value_errors); if (!$errors) { $editor = id(new PhabricatorConfigEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); try { $editor->applyTransactions($config_entry, array($xaction)); return id(new AphrontRedirectResponse())->setURI($done_uri); } catch (PhabricatorConfigValidationException $ex) { $e_value = pht('Invalid'); $errors[] = $ex->getMessage(); } } } else { if ($config_entry->getIsDeleted()) { $display_value = null; } else { $display_value = $this->getDisplayValue( $option, $config_entry, $config_entry->getValue()); } } $form = new AphrontFormView(); $error_view = null; if ($errors) { $error_view = id(new PHUIInfoView()) ->setErrors($errors); } else if ($option->getHidden()) { $msg = pht( 'This configuration is hidden and can not be edited or viewed from '. 'the web interface.'); $error_view = id(new PHUIInfoView()) ->setTitle(pht('Configuration Hidden')) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->appendChild(phutil_tag('p', array(), $msg)); } else if ($option->getLocked()) { $msg = $option->getLockedMessage(); $error_view = id(new PHUIInfoView()) ->setTitle(pht('Configuration Locked')) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(phutil_tag('p', array(), $msg)); } if ($option->getHidden()) { $control = null; } else { $control = $this->renderControl( $option, $display_value, $e_value); } $engine = new PhabricatorMarkupEngine(); - $engine->setViewer($user); + $engine->setViewer($viewer); $engine->addObject($option, 'description'); $engine->process(); $description = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($option, 'description')); $form - ->setUser($user) + ->setUser($viewer) ->addHiddenInput('issue', $request->getStr('issue')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Description')) ->setValue($description)); if ($group) { $extra = $group->renderContextualDescription( $option, $request); if ($extra !== null) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setValue($extra)); } } $form ->appendChild($control); $submit_control = id(new AphrontFormSubmitControl()) ->addCancelButton($done_uri); if (!$option->getLocked()) { $submit_control->setValue(pht('Save Config Entry')); } $form->appendChild($submit_control); $examples = $this->renderExamples($option); if ($examples) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Examples')) ->setValue($examples)); } if (!$option->getHidden()) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Default')) ->setValue($this->renderDefaults($option, $config_entry))); } - $title = pht('Edit %s', $this->key); + $title = pht('Edit %s', $key); $short = pht('Edit'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); if ($error_view) { $form_box->setInfoView($error_view); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Config'), $this->getApplicationURI()); if ($group) { $crumbs->addTextCrumb($group->getName(), $group_uri); } - $crumbs->addTextCrumb($this->key, '/config/edit/'.$this->key); + $crumbs->addTextCrumb($key, '/config/edit/'.$key); $timeline = $this->buildTransactionTimeline( $config_entry, new PhabricatorConfigTransactionQuery()); $timeline->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $form_box, $timeline, ), array( 'title' => $title, )); } private function readRequest( PhabricatorConfigOption $option, AphrontRequest $request) { $xaction = new PhabricatorConfigTransaction(); $xaction->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT); $e_value = null; $errors = array(); $value = $request->getStr('value'); if (!strlen($value)) { $value = null; $xaction->setNewValue( array( 'deleted' => true, 'value' => null, )); return array($e_value, $errors, $value, $xaction); } if ($option->isCustomType()) { $info = $option->getCustomObject()->readRequest($option, $request); list($e_value, $errors, $set_value, $value) = $info; } else { $type = $option->getType(); $set_value = null; switch ($type) { case 'int': if (preg_match('/^-?[0-9]+$/', trim($value))) { $set_value = (int)$value; } else { $e_value = pht('Invalid'); $errors[] = pht('Value must be an integer.'); } break; case 'string': case 'enum': $set_value = (string)$value; break; case 'list': case 'list': $set_value = phutil_split_lines( $request->getStr('value'), $retain_endings = false); foreach ($set_value as $key => $v) { if (!strlen($v)) { unset($set_value[$key]); } } $set_value = array_values($set_value); break; case 'set': $set_value = array_fill_keys($request->getStrList('value'), true); break; case 'bool': switch ($value) { case 'true': $set_value = true; break; case 'false': $set_value = false; break; default: $e_value = pht('Invalid'); $errors[] = pht('Value must be boolean, "true" or "false".'); break; } break; case 'class': if (!class_exists($value)) { $e_value = pht('Invalid'); $errors[] = pht('Class does not exist.'); } else { $base = $option->getBaseClass(); if (!is_subclass_of($value, $base)) { $e_value = pht('Invalid'); $errors[] = pht('Class is not of valid type.'); } else { $set_value = $value; } } break; default: $json = json_decode($value, true); if ($json === null && strtolower($value) != 'null') { $e_value = pht('Invalid'); $errors[] = pht( 'The given value must be valid JSON. This means, among '. 'other things, that you must wrap strings in double-quotes.'); } else { $set_value = $json; } break; } } if (!$errors) { $xaction->setNewValue( array( 'deleted' => false, 'value' => $set_value, )); } else { $xaction = null; } return array($e_value, $errors, $value, $xaction); } private function getDisplayValue( PhabricatorConfigOption $option, PhabricatorConfigEntry $entry, $value) { if ($option->isCustomType()) { return $option->getCustomObject()->getDisplayValue( $option, $entry, $value); } else { $type = $option->getType(); switch ($type) { case 'int': case 'string': case 'enum': case 'class': return $value; case 'bool': return $value ? 'true' : 'false'; case 'list': case 'list': return implode("\n", nonempty($value, array())); case 'set': return implode("\n", nonempty(array_keys($value), array())); default: return PhabricatorConfigJSON::prettyPrintJSON($value); } } } private function renderControl( PhabricatorConfigOption $option, $display_value, $e_value) { if ($option->isCustomType()) { $control = $option->getCustomObject()->renderControl( $option, $display_value, $e_value); } else { $type = $option->getType(); switch ($type) { case 'int': case 'string': $control = id(new AphrontFormTextControl()); break; case 'bool': $control = id(new AphrontFormSelectControl()) ->setOptions( array( '' => pht('(Use Default)'), 'true' => idx($option->getBoolOptions(), 0), 'false' => idx($option->getBoolOptions(), 1), )); break; case 'enum': $options = array_mergev( array( array('' => pht('(Use Default)')), $option->getEnumOptions(), )); $control = id(new AphrontFormSelectControl()) ->setOptions($options); break; case 'class': $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass($option->getBaseClass()) ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); $names = ipull($symbols, 'name', 'name'); asort($names); $names = array( '' => pht('(Use Default)'), ) + $names; $control = id(new AphrontFormSelectControl()) ->setOptions($names); break; case 'list': case 'list': $control = id(new AphrontFormTextAreaControl()) ->setCaption(pht('Separate values with newlines.')); break; case 'set': $control = id(new AphrontFormTextAreaControl()) ->setCaption(pht('Separate values with newlines or commas.')); break; default: $control = id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setCustomClass('PhabricatorMonospaced') ->setCaption(pht('Enter value in JSON.')); break; } $control ->setLabel(pht('Value')) ->setError($e_value) ->setValue($display_value) ->setName('value'); } if ($option->getLocked()) { $control->setDisabled(true); } return $control; } private function renderExamples(PhabricatorConfigOption $option) { $examples = $option->getExamples(); if (!$examples) { return null; } $table = array(); $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), pht('Example')), phutil_tag('th', array(), pht('Value')), )); foreach ($examples as $example) { list($value, $description) = $example; if ($value === null) { $value = phutil_tag('em', array(), pht('(empty)')); } else { if (is_array($value)) { $value = implode("\n", $value); } } $table[] = phutil_tag('tr', array(), array( phutil_tag('th', array(), $description), phutil_tag('td', array(), $value), )); } require_celerity_resource('config-options-css'); return phutil_tag( 'table', array( 'class' => 'config-option-table', ), $table); } private function renderDefaults( PhabricatorConfigOption $option, PhabricatorConfigEntry $entry) { $stack = PhabricatorEnv::getConfigSourceStack(); $stack = $stack->getStack(); $table = array(); $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), pht('Source')), phutil_tag('th', array(), pht('Value')), )); foreach ($stack as $key => $source) { $value = $source->getKeys( array( $option->getKey(), )); if (!array_key_exists($option->getKey(), $value)) { $value = phutil_tag('em', array(), pht('(empty)')); } else { $value = $this->getDisplayValue( $option, $entry, $value[$option->getKey()]); } $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), $source->getName()), phutil_tag('td', array(), $value), )); } require_celerity_resource('config-options-css'); return phutil_tag( 'table', array( 'class' => 'config-option-table', ), $table); } } diff --git a/src/applications/config/controller/PhabricatorConfigGroupController.php b/src/applications/config/controller/PhabricatorConfigGroupController.php index b9dffd9a08..4569491427 100644 --- a/src/applications/config/controller/PhabricatorConfigGroupController.php +++ b/src/applications/config/controller/PhabricatorConfigGroupController.php @@ -1,108 +1,102 @@ groupKey = $data['key']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $group_key = $request->getURIData('key'); $groups = PhabricatorApplicationConfigOptions::loadAll(); - $options = idx($groups, $this->groupKey); + $options = idx($groups, $group_key); if (!$options) { return new Aphront404Response(); } $title = pht('%s Configuration', $options->getName()); $list = $this->buildOptionList($options->getOptions()); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setObjectList($list); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Config'), $this->getApplicationURI()) ->addTextCrumb($options->getName(), $this->getApplicationURI()); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } private function buildOptionList(array $options) { assert_instances_of($options, 'PhabricatorConfigOption'); require_celerity_resource('config-options-css'); $db_values = array(); if ($options) { $db_values = id(new PhabricatorConfigEntry())->loadAllWhere( 'configKey IN (%Ls) AND namespace = %s', mpull($options, 'getKey'), 'default'); $db_values = mpull($db_values, null, 'getConfigKey'); } $engine = id(new PhabricatorMarkupEngine()) ->setViewer($this->getRequest()->getUser()); foreach ($options as $option) { $engine->addObject($option, 'summary'); } $engine->process(); $list = new PHUIObjectItemListView(); foreach ($options as $option) { $summary = $engine->getOutput($option, 'summary'); $item = id(new PHUIObjectItemView()) ->setHeader($option->getKey()) ->setHref('/config/edit/'.$option->getKey().'/') ->addAttribute($summary); if (!$option->getHidden()) { $current_value = PhabricatorEnv::getEnvConfig($option->getKey()); $current_value = PhabricatorConfigJSON::prettyPrintJSON( $current_value); $current_value = phutil_tag( 'div', array( 'class' => 'config-options-current-value', ), array( phutil_tag('span', array(), pht('Current Value:')), ' '.$current_value, )); $item->appendChild($current_value); } $db_value = idx($db_values, $option->getKey()); if ($db_value && !$db_value->getIsDeleted()) { $item->addIcon('edit', pht('Customized')); } if ($option->getHidden()) { $item->addIcon('unpublish', pht('Hidden')); } else if ($option->getLocked()) { $item->addIcon('lock', pht('Locked')); } $list->addItem($item); } return $list; } } diff --git a/src/applications/config/controller/PhabricatorConfigHistoryController.php b/src/applications/config/controller/PhabricatorConfigHistoryController.php index acb1d3c297..d86fb79878 100644 --- a/src/applications/config/controller/PhabricatorConfigHistoryController.php +++ b/src/applications/config/controller/PhabricatorConfigHistoryController.php @@ -1,52 +1,52 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $xactions = id(new PhabricatorConfigTransactionQuery()) - ->setViewer($user) + ->setViewer($viewer) ->needComments(true) ->execute(); $object = new PhabricatorConfigEntry(); $xaction = $object->getApplicationTransactionTemplate(); $view = $xaction->getApplicationTransactionViewObject(); $timeline = $view - ->setUser($user) + ->setUser($viewer) ->setTransactions($xactions) ->setRenderAsFeed(true) ->setObjectPHID(PhabricatorPHIDConstants::PHID_VOID); $timeline->setShouldTerminate(true); $object->willRenderTimeline($timeline, $this->getRequest()); $title = pht('Settings History'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); $crumbs->addTextCrumb('Config', $this->getApplicationURI()); $crumbs->addTextCrumb($title, '/config/history/'); $nav = $this->buildSideNavView(); $nav->selectFilter('history/'); $nav->setCrumbs($crumbs); $nav->appendChild($timeline); return $this->buildApplicationPage( array( $nav, ), array( 'title' => $title, )); } } diff --git a/src/applications/config/controller/PhabricatorConfigIgnoreController.php b/src/applications/config/controller/PhabricatorConfigIgnoreController.php index ba634dec1a..80a859c147 100644 --- a/src/applications/config/controller/PhabricatorConfigIgnoreController.php +++ b/src/applications/config/controller/PhabricatorConfigIgnoreController.php @@ -1,68 +1,63 @@ getViewer(); + $issue = $request->getURIData('key'); + $verb = $request->getURIData('verb'); - public function willProcessRequest(array $data) { - $this->verb = $data['verb']; - $this->issue = $data['key']; - } - - public function processRequest() { - $request = $this->getRequest(); - $issue_uri = $this->getApplicationURI('issue/'.$this->issue.'/'); + $issue_uri = $this->getApplicationURI('issue/'.$issue.'/'); if ($request->isDialogFormPost()) { - $this->manageApplication(); + $this->manageApplication($issue); return id(new AphrontRedirectResponse())->setURI($issue_uri); } - if ($this->verb == 'ignore') { + if ($verb == 'ignore') { $title = pht('Really ignore this setup issue?'); $submit_title = pht('Ignore'); $body = pht( "You can ignore an issue if you don't want to fix it, or plan to ". "fix it later. Ignored issues won't appear on every page but will ". "still be shown in the list of open issues."); - } else if ($this->verb == 'unignore') { + } else if ($verb == 'unignore') { $title = pht('Unignore this setup issue?'); $submit_title = pht('Unignore'); $body = pht( 'This issue will no longer be suppressed, and will return to its '. 'rightful place as a global setup warning.'); } else { - throw new Exception(pht('Unrecognized verb: %s', $this->verb)); + throw new Exception(pht('Unrecognized verb: %s', $verb)); } $dialog = id(new AphrontDialogView()) ->setUser($request->getUser()) ->setTitle($title) ->appendChild($body) ->addSubmitButton($submit_title) ->addCancelButton($issue_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } - public function manageApplication() { + public function manageApplication($issue) { $key = 'config.ignore-issues'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $list = $config_entry->getValue(); - if (isset($list[$this->issue])) { - unset($list[$this->issue]); + if (isset($list[$issue])) { + unset($list[$issue]); } else { - $list[$this->issue] = true; + $list[$issue] = true; } PhabricatorConfigEditor::storeNewValue( $this->getRequest()->getUser(), $config_entry, $list, PhabricatorContentSource::newFromRequest($this->getRequest())); } } diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php index d1d19860b9..89b8ea7cd6 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueListController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php @@ -1,114 +1,113 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $nav = $this->buildSideNavView(); $nav->selectFilter('issue/'); $issues = PhabricatorSetupCheck::runAllChecks(); PhabricatorSetupCheck::setOpenSetupIssueKeys( PhabricatorSetupCheck::getUnignoredIssueKeys($issues)); $important = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_IMPORTANT); $php = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_PHP); $mysql = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_MYSQL); $other = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_OTHER); $setup_issues = array(); if ($important) { $setup_issues[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Important Setup Issues')) ->setColor(PHUIObjectBoxView::COLOR_RED) ->setObjectList($important); } if ($php) { $setup_issues[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('PHP Setup Issues')) ->setObjectList($php); } if ($mysql) { $setup_issues[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('MySQL Setup Issues')) ->setObjectList($mysql); } if ($other) { $setup_issues[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Other Setup Issues')) ->setObjectList($other); } if (empty($setup_issues)) { $setup_issues[] = id(new PHUIInfoView()) ->setTitle(pht('No Issues')) ->appendChild( pht('Your install has no current setup issues to resolve.')) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); } $nav->appendChild($setup_issues); $title = pht('Setup Issues'); $crumbs = $this ->buildApplicationCrumbs($nav) ->addTextCrumb(pht('Setup'), $this->getApplicationURI('issue/')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function buildIssueList(array $issues, $group) { assert_instances_of($issues, 'PhabricatorSetupIssue'); $list = new PHUIObjectItemListView(); $ignored_items = array(); $items = 0; foreach ($issues as $issue) { if ($issue->getGroup() == $group) { $items++; $href = $this->getApplicationURI('/issue/'.$issue->getIssueKey().'/'); $item = id(new PHUIObjectItemView()) ->setHeader($issue->getName()) ->setHref($href) ->addAttribute($issue->getSummary()); if (!$issue->getIsIgnored()) { $item->setStatusIcon('fa-warning yellow'); $list->addItem($item); } else { $item->addIcon('fa-eye-slash', pht('Ignored')); $item->setDisabled(true); $item->setStatusIcon('fa-warning grey'); $ignored_items[] = $item; } } } foreach ($ignored_items as $item) { $list->addItem($item); } if ($items == 0) { return null; } else { return $list; } } } diff --git a/src/applications/config/controller/PhabricatorConfigIssueViewController.php b/src/applications/config/controller/PhabricatorConfigIssueViewController.php index a2beb8ae76..e8d6e188a4 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueViewController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueViewController.php @@ -1,71 +1,65 @@ issueKey = $data['key']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $issue_key = $request->getURIData('key'); $issues = PhabricatorSetupCheck::runAllChecks(); PhabricatorSetupCheck::setOpenSetupIssueKeys( PhabricatorSetupCheck::getUnignoredIssueKeys($issues)); - if (empty($issues[$this->issueKey])) { + if (empty($issues[$issue_key])) { $content = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Issue Resolved')) ->appendChild(pht('This setup issue has been resolved. ')) ->appendChild( phutil_tag( 'a', array( 'href' => $this->getApplicationURI('issue/'), ), pht('Return to Open Issue List'))); $title = pht('Resolved Issue'); } else { - $issue = $issues[$this->issueKey]; + $issue = $issues[$issue_key]; $content = $this->renderIssue($issue); $title = $issue->getShortName(); } $crumbs = $this ->buildApplicationCrumbs() ->setBorder(true) ->addTextCrumb(pht('Setup Issues'), $this->getApplicationURI('issue/')) ->addTextCrumb($title, $request->getRequestURI()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $title, )); } private function renderIssue(PhabricatorSetupIssue $issue) { require_celerity_resource('setup-issue-css'); $view = new PhabricatorSetupIssueView(); $view->setIssue($issue); $container = phutil_tag( 'div', array( 'class' => 'setup-issue-background', ), $view->render()); return $container; } } diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php index 4da4f2ac01..6a1823ecda 100644 --- a/src/applications/config/controller/PhabricatorConfigListController.php +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -1,65 +1,64 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $groups = PhabricatorApplicationConfigOptions::loadAll(); $core_list = $this->buildConfigOptionsList($groups, 'core'); $apps_list = $this->buildConfigOptionsList($groups, 'apps'); $title = pht('Phabricator Configuration'); $core = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setObjectList($core_list); $apps = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Applications Configuration')) ->setObjectList($apps_list); $nav->appendChild( array( $core, $apps, )); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Config'), $this->getApplicationURI()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function buildConfigOptionsList(array $groups, $type) { assert_instances_of($groups, 'PhabricatorApplicationConfigOptions'); $list = new PHUIObjectItemListView(); $groups = msort($groups, 'getName'); foreach ($groups as $group) { if ($group->getGroup() == $type) { $item = id(new PHUIObjectItemView()) ->setHeader($group->getName()) ->setHref('/config/group/'.$group->getKey().'/') ->addAttribute($group->getDescription()) ->setFontIcon($group->getFontIcon()); $list->addItem($item); } } return $list; } } diff --git a/src/applications/config/controller/PhabricatorConfigWelcomeController.php b/src/applications/config/controller/PhabricatorConfigWelcomeController.php index 11132af08b..37442c2375 100644 --- a/src/applications/config/controller/PhabricatorConfigWelcomeController.php +++ b/src/applications/config/controller/PhabricatorConfigWelcomeController.php @@ -1,422 +1,421 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $nav = $this->buildSideNavView(); $nav->selectFilter('welcome/'); $title = pht('Welcome'); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Welcome')); $nav->setCrumbs($crumbs); $nav->appendChild($this->buildWelcomeScreen($request)); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } public function buildWelcomeScreen(AphrontRequest $request) { $viewer = $request->getUser(); $this->requireResource('config-welcome-css'); $content = pht( "=== Install Phabricator ===\n\n". "You have successfully installed Phabricator. This screen will guide ". "you through configuration and orientation. ". "These steps are optional, and you can go through them in any order. ". "If you want to get back to this screen later on, you can find it in ". "the **Config** application under **Welcome Screen**."); $setup = array(); $setup[] = $this->newItem( $request, 'fa-check-square-o green', $content); $issues_resolved = !PhabricatorSetupCheck::getOpenSetupIssueKeys(); $setup_href = PhabricatorEnv::getURI('/config/issue/'); if ($issues_resolved) { $content = pht( "=== Resolve Setup Issues ===\n\n". "You've resolved (or ignored) all outstanding setup issues. ". "You can review issues in the **Config** application, under ". "**[[ %s | Setup Issues ]]**.", $setup_href); $icon = 'fa-check-square-o green'; } else { $content = pht( "=== Resolve Setup Issues ===\n\n". "You have some unresolved setup issues to take care of. Click ". "the link in the yellow banner at the top of the screen to see ". "them, or find them in the **Config** application under ". "**[[ %s | Setup Issues ]]**. ". "Although most setup issues should be resolved, sometimes an issue ". "is not applicable to an install. ". "If you don't intend to fix a setup issue (or don't want to fix ". "it for now), you can use the \"Ignore\" action to mark it as ". "something you don't plan to deal with.", $setup_href); $icon = 'fa-warning red'; } $setup[] = $this->newItem( $request, $icon, $content); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->execute(); $auth_href = PhabricatorEnv::getURI('/auth/'); $have_auth = (bool)$configs; if ($have_auth) { $content = pht( "=== Login and Registration ===\n\n". "You've configured at least one authentication provider, so users ". "can register or log in. ". "To configure more providers or adjust settings, use the ". "**[[ %s | Auth Application ]]**.", $auth_href); $icon = 'fa-check-square-o green'; } else { $content = pht( "=== Login and Registration ===\n\n". "You haven't configured any authentication providers yet. ". "Authentication providers allow users to register accounts and ". "log in to Phabricator. You can configure Phabricator to accept ". "credentials like username and password, LDAP, or Google OAuth. ". "You can configure authentication using the ". "**[[ %s | Auth Application ]]**.", $auth_href); $icon = 'fa-warning red'; } $setup[] = $this->newItem( $request, $icon, $content); $config_href = PhabricatorEnv::getURI('/config/'); // Just load any config value at all; if one exists the install has figured // out how to configure things. $have_config = (bool)id(new PhabricatorConfigEntry())->loadAllWhere( '1 = 1 LIMIT 1'); if ($have_config) { $content = pht( "=== Configure Phabricator Settings ===\n\n". "You've configured at least one setting from the web interface. ". "To configure more settings later, use the ". "**[[ %s | Config Application ]]**.", $config_href); $icon = 'fa-check-square-o green'; } else { $content = pht( "=== Configure Phabricator Settings ===\n\n". 'Many aspects of Phabricator are configurable. To explore and '. 'adjust settings, use the **[[ %s | Config Application ]]**.', $config_href); $icon = 'fa-info-circle'; } $setup[] = $this->newItem( $request, $icon, $content); $settings_href = PhabricatorEnv::getURI('/settings/'); $prefs = $viewer->loadPreferences()->getPreferences(); $have_settings = !empty($prefs); if ($have_settings) { $content = pht( "=== Adjust Account Settings ===\n\n". "You've adjusted at least one setting on your account. ". "To make more adjustments, visit the ". "**[[ %s | Settings Application ]]**.", $settings_href); $icon = 'fa-check-square-o green'; } else { $content = pht( "=== Adjust Account Settings ===\n\n". 'You can configure settings for your account by clicking the '. 'wrench icon in the main menu bar, or visiting the '. '**[[ %s | Settings Application ]]** directly.', $settings_href); $icon = 'fa-info-circle'; } $setup[] = $this->newItem( $request, $icon, $content); $dashboard_href = PhabricatorEnv::getURI('/dashboard/'); $have_dashboard = (bool)PhabricatorDashboardInstall::getDashboard( $viewer, PhabricatorHomeApplication::DASHBOARD_DEFAULT, 'PhabricatorHomeApplication'); if ($have_dashboard) { $content = pht( "=== Customize Home Page ===\n\n". "You've installed a default dashboard to replace this welcome screen ". "on the home page. ". "You can still visit the welcome screen here at any time if you ". "have steps you want to complete later, or if you feel lonely. ". "If you've changed your mind about the dashboard you installed, ". "you can install a different default dashboard with the ". "**[[ %s | Dashboards Application ]]**.", $dashboard_href); $icon = 'fa-check-square-o green'; } else { $content = pht( "=== Customize Home Page ===\n\n". "When you're done setting things up, you can create a custom ". "dashboard and install it. Your dashboard will replace this ". "welcome screen on the Phabricator home page. ". "Dashboards can show users the information that's most important to ". "your organization. You can configure them to display things like: ". "a custom welcome message, a feed of recent activity, or a list of ". "open tasks, waiting reviews, recent commits, and so on. ". "After you install a default dashboard, it will replace this page. ". "You can find this page later by visiting the **Config** ". "application, under **Welcome Page**. ". "To get started building a dashboard, use the ". "**[[ %s | Dashboards Application ]]**. ", $dashboard_href); $icon = 'fa-info-circle'; } $setup[] = $this->newItem( $request, $icon, $content); $apps_href = PhabricatorEnv::getURI('/applications/'); $content = pht( "=== Explore Applications ===\n\n". "Phabricator is a large suite of applications that work together to ". "help you develop software, manage tasks, and communicate. A few of ". "the most commonly used applications are pinned to the left navigation ". "bar by default.\n\n". "To explore all of the Phabricator applications, adjust settings, or ". "uninstall applications you don't plan to use, visit the ". "**[[ %s | Applications Application ]]**. You can also click the ". "**Applications** button in the left navigation menu, or search for an ". "application by name in the main menu bar. ", $apps_href); $explore = array(); $explore[] = $this->newItem( $request, 'fa-globe', $content); $support_href = PhabricatorEnv::getDoclink('Give Feedback! Get Support!'); $content = pht( "=== Need Help with Setup? ===\n\n". 'Having trouble getting something set up? See '. '**[[ %s | Give Feedback! Get Support! ]]** for ways to get in touch '. 'to get answers to questions, report bugs, and request features.', $support_href); $explore[] = $this->newItem( $request, 'fa-life-ring', $content); $differential_uri = PhabricatorEnv::getURI('/differential/'); $differential_create_uri = PhabricatorEnv::getURI( '/differential/diff/create/'); $differential_all_uri = PhabricatorEnv::getURI('/differential/query/all/'); $differential_user_guide = PhabricatorEnv::getDoclink( 'Differential User Guide'); $differential_vs_uri = PhabricatorEnv::getDoclink( 'User Guide: Review vs Audit'); $quick = array(); $quick[] = $this->newItem( $request, 'fa-gear', pht( "=== Quick Start: Code Review ===\n\n". "Review code with **[[ %s | Differential ]]**. ". "Engineers can use Differential to share, review, and approve ". "changes to source code. ". "To get started with code review:\n\n". " - **[[ %s | Create a Revision ]]** //(Copy and paste a diff from ". " the command line into the web UI to quickly get a feel for ". " review.)//\n". " - **[[ %s | View All Revisions ]]**\n\n". "For more information, see these articles in the documentation:\n\n". " - **[[ %s | Differential User Guide ]]**, for a general overview ". " of Differential.\n". " - **[[ %s | User Guide: Review vs Audit ]]**, for a discussion ". " of different code review workflows.", $differential_uri, $differential_create_uri, $differential_all_uri, $differential_user_guide, $differential_vs_uri)); $maniphest_uri = PhabricatorEnv::getURI('/maniphest/'); $maniphest_create_uri = PhabricatorEnv::getURI('/maniphest/task/create/'); $maniphest_all_uri = PhabricatorEnv::getURI('/maniphest/query/all/'); $quick[] = $this->newItem( $request, 'fa-anchor', pht( "=== Quick Start: Bugs and Tasks ===\n\n". "Track bugs and tasks in Phabricator with ". "**[[ %s | Maniphest ]]**. ". "Users in all roles can use Maniphest to manage current and ". "planned work and to track bugs and issues. ". "To get started with bugs and tasks:\n\n". " - **[[ %s | Create a Task ]]**\n". " - **[[ %s | View All Tasks ]]**\n", $maniphest_uri, $maniphest_create_uri, $maniphest_all_uri)); $pholio_uri = PhabricatorEnv::getURI('/pholio/'); $pholio_create_uri = PhabricatorEnv::getURI('/pholio/new/'); $pholio_all_uri = PhabricatorEnv::getURI('/pholio/query/all/'); $quick[] = $this->newItem( $request, 'fa-camera-retro', pht( "=== Quick Start: Design Review ===\n\n". "Review proposed designs with **[[ %s | Pholio ]]**. ". "Designers can use Pholio to share images of what they're working on ". "and show off things they've made. ". "To get started with design review:\n\n". " - **[[ %s | Create a Mock ]]**\n". " - **[[ %s | View All Mocks ]]**", $pholio_uri, $pholio_create_uri, $pholio_all_uri)); $diffusion_uri = PhabricatorEnv::getURI('/diffusion/'); $diffusion_create_uri = PhabricatorEnv::getURI('/diffusion/create/'); $diffusion_all_uri = PhabricatorEnv::getURI('/diffusion/query/all/'); $diffusion_user_guide = PhabricatorEnv::getDoclink('Diffusion User Guide'); $diffusion_setup_guide = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Repository Hosting'); $quick[] = $this->newItem( $request, 'fa-code', pht( "=== Quick Start: Repositories ===\n\n". "Manage and browse source code repositories with ". "**[[ %s | Diffusion ]]**. ". "Engineers can use Diffusion to browse and audit source code. ". "You can configure Phabricator to host repositories, or have it ". "track existing repositories hosted elsewhere (like GitHub, ". "Bitbucket, or an internal server). ". "To get started with repositories:\n\n". " - **[[ %s | Create a New Repository ]]**\n". " - **[[ %s | View All Repositories ]]**\n\n". "For more information, see these articles in the documentation:\n\n". " - **[[ %s | Diffusion User Guide ]]**, for a general overview of ". " Diffusion.\n". " - **[[ %s | Diffusion User Guide: Repository Hosting ]]**, ". " for instructions on configuring repository hosting.\n\n". "Phabricator supports Git, Mercurial and Subversion.", $diffusion_uri, $diffusion_create_uri, $diffusion_all_uri, $diffusion_user_guide, $diffusion_setup_guide)); $header = id(new PHUIHeaderView()) ->setHeader(pht('Welcome to Phabricator')); $setup_header = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent(pht('=Setup and Configuration')), 'default', $viewer); $explore_header = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent(pht('=Explore Phabricator')), 'default', $viewer); $quick_header = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent(pht('=Quick Start Guides')), 'default', $viewer); return id(new PHUIDocumentView()) ->setHeader($header) ->setFluid(true) ->appendChild($setup_header) ->appendChild($setup) ->appendChild($explore_header) ->appendChild($explore) ->appendChild($quick_header) ->appendChild($quick); } private function newItem(AphrontRequest $request, $icon, $content) { $viewer = $request->getUser(); $icon = id(new PHUIIconView()) ->setIconFont($icon.' fa-2x'); $content = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($content), 'default', $viewer); $icon = phutil_tag( 'div', array( 'class' => 'config-welcome-icon', ), $icon); $content = phutil_tag( 'div', array( 'class' => 'config-welcome-content', ), $content); $view = phutil_tag( 'div', array( 'class' => 'config-welcome-box grouped', ), array( $icon, $content, )); return $view; } } diff --git a/src/applications/conpherence/controller/ConpherenceNewRoomController.php b/src/applications/conpherence/controller/ConpherenceNewRoomController.php index 5578741e89..40dd6b2572 100644 --- a/src/applications/conpherence/controller/ConpherenceNewRoomController.php +++ b/src/applications/conpherence/controller/ConpherenceNewRoomController.php @@ -1,131 +1,133 @@ getUser(); $title = pht('New Room'); $e_title = true; + $v_message = null; $validation_exception = null; $conpherence = ConpherenceThread::initializeNewRoom($user); $participants = array(); if ($request->isFormPost()) { $editor = new ConpherenceEditor(); $xactions = array(); $participants = $request->getArr('participants'); $participants[] = $user->getPHID(); $participants = array_unique($participants); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransaction::TYPE_PARTICIPANTS) ->setNewValue(array('+' => $participants)); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransaction::TYPE_TITLE) ->setNewValue($request->getStr('title')); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY) ->setNewValue($request->getStr('joinPolicy')); - $message = $request->getStr('message'); - if ($message) { + $v_message = $request->getStr('message'); + if (strlen($v_message)) { $message_xactions = $editor->generateTransactionsFromText( $user, $conpherence, - $message); + $v_message); $xactions = array_merge($xactions, $message_xactions); } try { $editor ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($user) ->applyTransactions($conpherence, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/'.$conpherence->getMonogram()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_title = $ex->getShortMessage(ConpherenceTransaction::TYPE_TITLE); $conpherence->setViewPolicy($request->getStr('viewPolicy')); $conpherence->setEditPolicy($request->getStr('editPolicy')); $conpherence->setJoinPolicy($request->getStr('joinPolicy')); } } else { if ($request->getStr('participant')) { $participants[] = $request->getStr('participant'); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($conpherence) ->execute(); $submit_uri = $this->getApplicationURI('new/'); $cancel_uri = $this->getApplicationURI('search/'); $dialog = $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setValidationException($validation_exception) ->setUser($user) ->setTitle($title) ->addCancelButton($cancel_uri) ->addSubmitButton(pht('Create Room')); $form = id(new PHUIFormLayoutView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setError($e_title) ->setLabel(pht('Name')) ->setName('title') ->setValue($request->getStr('title'))) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('participants') ->setUser($user) ->setDatasource(new PhabricatorPeopleDatasource()) ->setValue($participants) ->setLabel(pht('Other Participants'))) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($conpherence) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($conpherence) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('joinPolicy') ->setPolicyObject($conpherence) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN) ->setPolicies($policies)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($user) ->setName('message') - ->setLabel(pht('First Message'))); + ->setLabel(pht('First Message')) + ->setValue($v_message)); $dialog->appendChild($form); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php index 5dfbd18146..81129f3beb 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php @@ -1,51 +1,45 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $countdown = id(new PhabricatorCountdownQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$countdown) { return new Aphront404Response(); } if ($request->isFormPost()) { $countdown->delete(); return id(new AphrontRedirectResponse()) ->setURI('/countdown/'); } $inst = pht( 'Are you sure you want to delete the countdown %s?', $countdown->getTitle()); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle(pht('Really delete this countdown?')); $dialog->appendChild(phutil_tag('p', array(), $inst)); $dialog->addSubmitButton(pht('Delete')); $dialog->addCancelButton('/countdown/'); $dialog->setSubmitURI($request->getPath()); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/countdown/controller/PhabricatorCountdownListController.php b/src/applications/countdown/controller/PhabricatorCountdownListController.php index f3c34d23aa..031e5430fe 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownListController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownListController.php @@ -1,26 +1,22 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhabricatorCountdownSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/countdown/editor/PhabricatorCountdownEditor.php b/src/applications/countdown/editor/PhabricatorCountdownEditor.php index 7438e4ac02..97f8e5b947 100644 --- a/src/applications/countdown/editor/PhabricatorCountdownEditor.php +++ b/src/applications/countdown/editor/PhabricatorCountdownEditor.php @@ -1,206 +1,215 @@ getTransactionType()) { case PhabricatorCountdownTransaction::TYPE_TITLE: return $object->getTitle(); case PhabricatorCountdownTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhabricatorCountdownTransaction::TYPE_EPOCH: return $object->getEpoch(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorCountdownTransaction::TYPE_TITLE: return $xaction->getNewValue(); case PhabricatorCountdownTransaction::TYPE_DESCRIPTION: return $xaction->getNewValue(); case PhabricatorCountdownTransaction::TYPE_EPOCH: return $xaction->getNewValue()->getEpoch(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorCountdownTransaction::TYPE_TITLE: $object->setTitle($xaction->getNewValue()); return; case PhabricatorCountdownTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; case PhabricatorCountdownTransaction::TYPE_EPOCH: $object->setEpoch($xaction->getNewValue()); return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorCountdownTransaction::TYPE_TITLE: return; case PhabricatorCountdownTransaction::TYPE_DESCRIPTION: return; case PhabricatorCountdownTransaction::TYPE_EPOCH: return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorCountdownTransaction::TYPE_TITLE: $missing = $this->validateIsEmptyTextField( $object->getTitle(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('You must give the countdown a name.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; case PhabricatorCountdownTransaction::TYPE_EPOCH: $date_value = AphrontFormDateControlValue::newFromEpoch( $this->requireActor(), $object->getEpoch()); if (!$date_value->isValid()) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('You must give the countdown a valid end date.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; } return $errors; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } public function getMailTagsMap() { return array( PhabricatorCountdownTransaction::MAILTAG_TITLE => pht('Someone changes the countdown title.'), PhabricatorCountdownTransaction::MAILTAG_DESCRIPTION => pht('Someone changes the countdown description.'), PhabricatorCountdownTransaction::MAILTAG_EPOCH => pht('Someone changes the countdown end date.'), PhabricatorCountdownTransaction::MAILTAG_OTHER => pht('Other countdown activity not listed above occurs.'), ); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $monogram = $object->getMonogram(); - $name = $object->getName(); + $name = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("{$monogram}: {$name}") ->addHeader('Thread-Topic', $monogram); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); + $description = $object->getDescription(); + + if (strlen($description)) { + $body->addTextSection( + pht('COUNTDOWN DESCRIPTION'), + $object->getDescription()); + } $body->addLinkSection( pht('COUNTDOWN DETAIL'), PhabricatorEnv::getProductionURI('/'.$object->getMonogram())); return $body; } protected function getMailTo(PhabricatorLiskDAO $object) { - return array($object->getAuthorPHID()); + return array( + $object->getAuthorPHID(), + $this->requireActor()->getPHID(), + ); } - protected function getMailSubjectPrefix() { - return 'Countdown'; + return '[Countdown]'; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhabricatorCountdownReplyHandler()) ->setMailReceiver($object); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } } diff --git a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php new file mode 100644 index 0000000000..d0218de59b --- /dev/null +++ b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php @@ -0,0 +1,28 @@ +setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + } + + protected function getTransactionReplyHandler() { + return new PhabricatorCountdownReplyHandler(); + } + +} diff --git a/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php b/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php index 74ee2b9518..b9f438ece8 100644 --- a/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php +++ b/src/applications/countdown/query/PhabricatorCountdownSearchEngine.php @@ -1,145 +1,148 @@ newQuery(); if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') { $query->withUpcoming(); } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorUsersSearchField()) ->setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setAliases(array('author', 'authors')), id(new PhabricatorSearchCheckboxesField()) ->setKey('upcoming') ->setOptions(array( 'upcoming' => pht('Show only upcoming countdowns.'), )), ); } protected function getURI($path) { return '/countdown/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'upcoming' => pht('Upcoming'), 'all' => pht('All'), ); if ($this->requireViewer()->getPHID()) { $names['authored'] = pht('Authored'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'authored': return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); case 'upcoming': return $query->setParameter('upcoming', array('upcoming')); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $countdowns, PhabricatorSavedQuery $query) { return mpull($countdowns, 'getAuthorPHID'); } protected function renderResultList( array $countdowns, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($countdowns, 'PhabricatorCountdown'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($countdowns as $countdown) { $id = $countdown->getID(); $ended = false; - $icon = 'fa-clock-o'; - $color = 'green'; $epoch = $countdown->getEpoch(); if ($epoch <= PhabricatorTime::getNow()) { $ended = true; - $icon = 'fa-check-square-o'; - $color = 'grey'; } $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setObject($countdown) ->setObjectName("C{$id}") ->setHeader($countdown->getTitle()) - ->setStatusIcon($icon.' '.$color) ->setHref($this->getApplicationURI("{$id}/")) ->addByline( pht( 'Created by %s', $handles[$countdown->getAuthorPHID()]->renderLink())); if ($ended) { $item->addAttribute( pht('Launched on %s', phabricator_datetime($epoch, $viewer))); $item->setDisabled(true); } else { + $time_left = ($epoch - PhabricatorTime::getNow()); + $num = round($time_left / (60 * 60 * 24)); + $noun = pht('Days'); + if ($num < 1) { + $num = round($time_left / (60 * 60), 1); + $noun = pht('Hours'); + } + $item->setCountdown($num, $noun); $item->addAttribute( phabricator_datetime($epoch, $viewer)); } $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No countdowns found.')); return $result; } } diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index 01fd5d2509..8753c66223 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -1,136 +1,144 @@ setViewer($actor) ->withClasses(array('PhabricatorCountdownApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorCountdownDefaultViewCapability::CAPABILITY); return id(new PhabricatorCountdown()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEpoch(PhabricatorTime::getNow()) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'description' => 'text', + 'mailKey' => 'bytes20', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCountdownCountdownPHIDType::TYPECONST); } public function getMonogram() { return 'C'.$this->getID(); } + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCountdownEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorCountdownTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( 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) { return false; } public function describeAutomaticCapability($capability) { return false; } /* -( PhabricatorSpacesInterface )------------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/countdown/storage/PhabricatorCountdownTransaction.php b/src/applications/countdown/storage/PhabricatorCountdownTransaction.php index e3241acd63..eace665a89 100644 --- a/src/applications/countdown/storage/PhabricatorCountdownTransaction.php +++ b/src/applications/countdown/storage/PhabricatorCountdownTransaction.php @@ -1,155 +1,180 @@ getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created this countdown.', $this->renderHandleLink($author_phid)); } else { return pht( '%s renamed this countdown from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_DESCRIPTION: if ($old === null) { return pht( '%s set the description of this countdown.', $this->renderHandleLink($author_phid)); } else { return pht( '%s edited the description of this countdown.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_EPOCH: if ($old === null) { return pht( '%s set this countdown to end on %s.', $this->renderHandleLink($author_phid), phabricator_datetime($new, $this->getViewer())); } else if ($old != $new) { return pht( '%s updated this countdown to end on %s.', $this->renderHandleLink($author_phid), phabricator_datetime($new, $this->getViewer())); } break; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s renamed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; case self::TYPE_DESCRIPTION: if ($old === null) { return pht( '%s set the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s edited the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; case self::TYPE_EPOCH: if ($old === null) { return pht( '%s set the end date of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s edited the end date of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; } return parent::getTitleForFeed(); } public function getMailTags() { $tags = parent::getMailTags(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: $tags[] = self::MAILTAG_TITLE; break; case self::TYPE_EPOCH: $tags[] = self::MAILTAG_EPOCH; break; case self::TYPE_DESCRIPTION: $tags[] = self::MAILTAG_DESCRIPTION; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } + public function shouldHide() { + $old = $this->getOldValue(); + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return ($old === null); + } + return parent::shouldHide(); + } + + public function hasChangeDetails() { + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return ($this->getOldValue() !== null); + } + + return parent::hasChangeDetails(); + } + + public function renderChangeDetails(PhabricatorUser $viewer) { + return $this->renderTextCorpusChangeDetails( + $viewer, + $this->getOldValue(), + $this->getNewValue()); + } + } diff --git a/src/applications/countdown/view/PhabricatorCountdownView.php b/src/applications/countdown/view/PhabricatorCountdownView.php index 7ede4ef887..2e8f1dedb3 100644 --- a/src/applications/countdown/view/PhabricatorCountdownView.php +++ b/src/applications/countdown/view/PhabricatorCountdownView.php @@ -1,79 +1,90 @@ headless = $headless; return $this; } public function setCountdown(PhabricatorCountdown $countdown) { $this->countdown = $countdown; return $this; } protected function getTagContent() { $countdown = $this->countdown; require_celerity_resource('phabricator-countdown-css'); $header = null; if (!$this->headless) { $header = phutil_tag( 'div', array( 'class' => 'phabricator-timer-header', ), array( 'C'.$countdown->getID(), ' ', phutil_tag( 'a', array( 'href' => '/countdown/'.$countdown->getID(), ), $countdown->getTitle()), )); } $ths = array( phutil_tag('th', array(), pht('Days')), phutil_tag('th', array(), pht('Hours')), phutil_tag('th', array(), pht('Minutes')), phutil_tag('th', array(), pht('Seconds')), ); $dashes = array( javelin_tag('td', array('sigil' => 'phabricator-timer-days'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-hours'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-minutes'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-seconds'), '-'), ); + $epoch = $countdown->getEpoch(); + $launch_date = phabricator_datetime($epoch, $this->getUser()); + $foot = phutil_tag( + 'td', + array( + 'colspan' => '4', + 'class' => 'phabricator-timer-foot', + ), + $launch_date); + $container = celerity_generate_unique_node_id(); $content = phutil_tag( 'div', array('class' => 'phabricator-timer', 'id' => $container), array( $header, phutil_tag('table', array('class' => 'phabricator-timer-table'), array( phutil_tag('tr', array(), $ths), phutil_tag('tr', array(), $dashes), + phutil_tag('tr', array(), $foot), )), )); Javelin::initBehavior('countdown-timer', array( 'timestamp' => $countdown->getEpoch(), 'container' => $container, )); return $content; } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index beaabae074..9f54726bc9 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -1,272 +1,271 @@ 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, ); } $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()); } $completed_info = isort($completed_info, 'n'); $rows = array(); foreach ($completed_info as $class => $info) { $rows[] = array( $class, number_format($info['n']), pht('%s us', new PhutilNumber((int)($info['duration'] / $info['n']))), ); } 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, ); } $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'), )); $completed_table->setColumnClasses( array( 'wide', 'n', 'n', )); $completed_panel = new PHUIObjectBoxView(); $completed_panel->setHeaderText( pht('Recently Completed Tasks (Last 15m)')); $completed_panel->setTable($completed_table); $daemon_table = new PhabricatorDaemonLogListView(); $daemon_table->setUser($viewer); $daemon_table->setDaemonLogs($logs); $daemon_panel = new PHUIObjectBoxView(); $daemon_panel->setHeaderText(pht('Active Daemons')); $daemon_panel->setObjectList($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) ->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->buildApplicationPage( $nav, array( 'title' => pht('Console'), - 'device' => false, )); } 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/differential/customfield/DifferentialUnitField.php b/src/applications/differential/customfield/DifferentialUnitField.php index daff8cf0d5..b5de9fce53 100644 --- a/src/applications/differential/customfield/DifferentialUnitField.php +++ b/src/applications/differential/customfield/DifferentialUnitField.php @@ -1,196 +1,202 @@ getFieldName(); } protected function getLegacyProperty() { return 'arc:unit'; } protected function getDiffPropertyKeys() { return array( 'arc:unit', 'arc:unit-excuse', ); } protected function loadHarbormasterTargetMessages(array $target_phids) { return id(new HarbormasterBuildUnitMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); } protected function newModernMessage(array $message) { return HarbormasterBuildUnitMessage::newFromDictionary( new HarbormasterBuildTarget(), $this->getModernUnitMessageDictionary($message)); } protected function newHarbormasterMessageView(array $messages) { foreach ($messages as $key => $message) { - if ($message->getResult() == ArcanistUnitTestResult::RESULT_PASS) { - unset($messages[$key]); + switch ($message->getResult()) { + case ArcanistUnitTestResult::RESULT_PASS: + case ArcanistUnitTestResult::RESULT_SKIP: + // Don't show "Pass" or "Skip" in the UI since they aren't very + // interesting. The user can click through to the full results if + // they want details. + unset($messages[$key]); + break; } } if (!$messages) { return null; } return id(new HarbormasterUnitPropertyView()) ->setLimit(10) ->setUnitMessages($messages); } public function getWarningsForDetailView() { $status = $this->getObject()->getActiveDiff()->getUnitStatus(); $warnings = array(); if ($status < DifferentialUnitStatus::UNIT_WARN) { // Don't show any warnings. } else if ($status == DifferentialUnitStatus::UNIT_AUTO_SKIP) { // Don't show any warnings. } else if ($status == DifferentialUnitStatus::UNIT_POSTPONED) { $warnings[] = pht( 'Background tests have not finished executing on these changes.'); } else if ($status == DifferentialUnitStatus::UNIT_SKIP) { $warnings[] = pht( 'Unit tests were skipped when generating these changes.'); } else { $warnings[] = pht('These changes have unit test problems.'); } return $warnings; } protected function renderHarbormasterStatus( DifferentialDiff $diff, array $messages) { $colors = array( DifferentialUnitStatus::UNIT_NONE => 'grey', DifferentialUnitStatus::UNIT_OKAY => 'green', DifferentialUnitStatus::UNIT_WARN => 'yellow', DifferentialUnitStatus::UNIT_FAIL => 'red', DifferentialUnitStatus::UNIT_SKIP => 'blue', DifferentialUnitStatus::UNIT_AUTO_SKIP => 'blue', DifferentialUnitStatus::UNIT_POSTPONED => 'blue', ); $icon_color = idx($colors, $diff->getUnitStatus(), 'grey'); $message = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff); $note = array(); $groups = mgroup($messages, 'getResult'); $groups = array_select_keys( $groups, array( ArcanistUnitTestResult::RESULT_FAIL, ArcanistUnitTestResult::RESULT_BROKEN, ArcanistUnitTestResult::RESULT_UNSOUND, ArcanistUnitTestResult::RESULT_SKIP, ArcanistUnitTestResult::RESULT_PASS, )) + $groups; foreach ($groups as $result => $group) { $count = new PhutilNumber(count($group)); switch ($result) { case ArcanistUnitTestResult::RESULT_PASS: $note[] = pht('%s Passed Test(s)', $count); break; case ArcanistUnitTestResult::RESULT_FAIL: $note[] = pht('%s Failed Test(s)', $count); break; case ArcanistUnitTestResult::RESULT_SKIP: $note[] = pht('%s Skipped Test(s)', $count); break; case ArcanistUnitTestResult::RESULT_BROKEN: $note[] = pht('%s Broken Test(s)', $count); break; case ArcanistUnitTestResult::RESULT_UNSOUND: $note[] = pht('%s Unsound Test(s)', $count); break; default: $note[] = pht('%s Other Test(s)', $count); break; } } $buildable = $diff->getBuildable(); if ($buildable) { $full_results = '/harbormaster/unit/'.$buildable->getID().'/'; $note[] = phutil_tag( 'a', array( 'href' => $full_results, ), pht('View Full Results')); } $excuse = $diff->getProperty('arc:unit-excuse'); if (strlen($excuse)) { $excuse = array( phutil_tag('strong', array(), pht('Excuse:')), ' ', phutil_escape_html_newlines($excuse), ); $note[] = $excuse; } $note = phutil_implode_html(" \xC2\xB7 ", $note); $status = id(new PHUIStatusListView()) ->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_STAR, $icon_color) ->setTarget($message) ->setNote($note)); return $status; } private function getModernUnitMessageDictionary(array $map) { // Strip out `null` values to satisfy stricter typechecks. foreach ($map as $key => $value) { if ($value === null) { unset($map[$key]); } } // TODO: Remap more stuff here? return $map; } } diff --git a/src/applications/differential/editor/DifferentialDiffEditor.php b/src/applications/differential/editor/DifferentialDiffEditor.php index 235ab84c21..0058485e63 100644 --- a/src/applications/differential/editor/DifferentialDiffEditor.php +++ b/src/applications/differential/editor/DifferentialDiffEditor.php @@ -1,237 +1,238 @@ 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 inteneded 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() == HeraldAdapter::ACTION_BLOCK) { + 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/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index c73bb53956..2a8e907145 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1,1918 +1,1815 @@ getTransactionType() == $type_update) { return $xaction; } } return null; } public function setIsCloseByCommit($is_close_by_commit) { $this->isCloseByCommit = $is_close_by_commit; return $this; } public function getIsCloseByCommit() { return $this->isCloseByCommit; } public function setChangedPriorToCommitURI($uri) { $this->changedPriorToCommitURI = $uri; return $this; } public function getChangedPriorToCommitURI() { return $this->changedPriorToCommitURI; } public function setRepositoryPHIDOverride($phid_or_null) { $this->repositoryPHIDOverride = $phid_or_null; return $this; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = DifferentialTransaction::TYPE_ACTION; $types[] = DifferentialTransaction::TYPE_INLINE; $types[] = DifferentialTransaction::TYPE_STATUS; $types[] = DifferentialTransaction::TYPE_UPDATE; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_ACTION: return null; case DifferentialTransaction::TYPE_INLINE: return null; case DifferentialTransaction::TYPE_UPDATE: if ($this->getIsNewObject()) { return null; } else { return $object->getActiveDiff()->getPHID(); } } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_ACTION: case DifferentialTransaction::TYPE_UPDATE: return $xaction->getNewValue(); case DifferentialTransaction::TYPE_INLINE: return null; } return parent::getCustomTransactionNewValue($object, $xaction); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $actor_phid = $this->getActingAsPHID(); switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return $xaction->hasComment(); case DifferentialTransaction::TYPE_ACTION: $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; $action_type = $xaction->getNewValue(); switch ($action_type) { case DifferentialAction::ACTION_ACCEPT: case DifferentialAction::ACTION_REJECT: if ($action_type == DifferentialAction::ACTION_ACCEPT) { $new_status = DifferentialReviewerStatus::STATUS_ACCEPTED; } else { $new_status = DifferentialReviewerStatus::STATUS_REJECTED; } $actor = $this->getActor(); // These transactions can cause effects in two ways: by altering the // status of an existing reviewer; or by adding the actor as a new // reviewer. $will_add_reviewer = true; foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->hasAuthority($actor)) { if ($reviewer->getStatus() != $new_status) { return true; } } if ($reviewer->getReviewerPHID() == $actor_phid) { $will_add_reviwer = false; } } return $will_add_reviewer; case DifferentialAction::ACTION_CLOSE: return ($object->getStatus() != $status_closed); case DifferentialAction::ACTION_ABANDON: return ($object->getStatus() != $status_abandoned); case DifferentialAction::ACTION_RECLAIM: return ($object->getStatus() == $status_abandoned); case DifferentialAction::ACTION_REOPEN: return ($object->getStatus() == $status_closed); case DifferentialAction::ACTION_RETHINK: return ($object->getStatus() != $status_plan); case DifferentialAction::ACTION_REQUEST: return ($object->getStatus() != $status_review); case DifferentialAction::ACTION_RESIGN: foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->getReviewerPHID() == $actor_phid) { return true; } } return false; case DifferentialAction::ACTION_CLAIM: return ($actor_phid != $object->getAuthorPHID()); } } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return; case DifferentialTransaction::TYPE_UPDATE: if (!$this->getIsCloseByCommit()) { switch ($object->getStatus()) { case $status_revision: case $status_plan: case $status_abandoned: $object->setStatus($status_review); break; } } $diff = $this->requireDiff($xaction->getNewValue()); $object->setLineCount($diff->getLineCount()); if ($this->repositoryPHIDOverride !== false) { $object->setRepositoryPHID($this->repositoryPHIDOverride); } else { $object->setRepositoryPHID($diff->getRepositoryPHID()); } $object->attachActiveDiff($diff); // TODO: Update the `diffPHID` once we add that. return; case DifferentialTransaction::TYPE_ACTION: switch ($xaction->getNewValue()) { case DifferentialAction::ACTION_RESIGN: case DifferentialAction::ACTION_ACCEPT: case DifferentialAction::ACTION_REJECT: // These have no direct effects, and affect review status only // indirectly by altering reviewers with TYPE_EDGE transactions. return; case DifferentialAction::ACTION_ABANDON: $object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED); return; case DifferentialAction::ACTION_RETHINK: $object->setStatus($status_plan); return; case DifferentialAction::ACTION_RECLAIM: $object->setStatus($status_review); return; case DifferentialAction::ACTION_REOPEN: $object->setStatus($status_review); return; case DifferentialAction::ACTION_REQUEST: $object->setStatus($status_review); return; case DifferentialAction::ACTION_CLOSE: $object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED); return; case DifferentialAction::ACTION_CLAIM: $object->setAuthorPHID($this->getActingAsPHID()); return; default: throw new Exception( pht( 'Differential action "%s" is not a valid action!', $xaction->getNewValue())); } break; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $results = parent::expandTransaction($object, $xaction); $actor = $this->getActor(); $actor_phid = $this->getActingAsPHID(); $type_edge = PhabricatorTransactions::TYPE_EDGE; $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; $edge_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST; $is_sticky_accept = PhabricatorEnv::getEnvConfig( 'differential.sticky-accept'); $downgrade_rejects = false; $downgrade_accepts = false; if ($this->getIsCloseByCommit()) { // Never downgrade reviewers when we're closing a revision after a // commit. } else { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: $downgrade_rejects = true; if (!$is_sticky_accept) { // If "sticky accept" is disabled, also downgrade the accepts. $downgrade_accepts = true; } break; case DifferentialTransaction::TYPE_ACTION: switch ($xaction->getNewValue()) { case DifferentialAction::ACTION_REQUEST: $downgrade_rejects = true; if ((!$is_sticky_accept) || ($object->getStatus() != $status_plan)) { // If the old state isn't "changes planned", downgrade the // accepts. This exception allows an accepted revision to // go through Plan Changes -> Request Review to return to // "accepted" if the author didn't update the revision. $downgrade_accepts = true; } break; } break; } } $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED; $new_reject = DifferentialReviewerStatus::STATUS_REJECTED; $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER; $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER; if ($downgrade_rejects || $downgrade_accepts) { // When a revision is updated, change all "reject" to "rejected older // revision". This means we won't immediately push the update back into // "needs review", but outstanding rejects will still block it from // moving to "accepted". // We also do this for "Request Review", even though the diff is not // updated directly. Essentially, this acts like an update which doesn't // actually change the diff text. $edits = array(); foreach ($object->getReviewerStatus() as $reviewer) { if ($downgrade_rejects) { if ($reviewer->getStatus() == $new_reject) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => array( 'status' => $old_reject, ), ); } } if ($downgrade_accepts) { if ($reviewer->getStatus() == $new_accept) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => array( 'status' => $old_accept, ), ); } } } if ($edits) { $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => $edits)); } } switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: if ($this->getIsCloseByCommit()) { // Don't bother with any of this if this update is a side effect of // commit detection. break; } // When a revision is updated and the diff comes from a branch named // "T123" or similar, automatically associate the commit with the // task that the branch names. $maniphest = 'PhabricatorManiphestApplication'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $diff = $this->requireDiff($xaction->getNewValue()); $branch = $diff->getBranch(); // No "$", to allow for branches like T123_demo. $match = null; if (preg_match('/^T(\d+)/i', $branch, $match)) { $task_id = $match[1]; $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array($task_id)) ->execute(); if ($tasks) { $task = head($tasks); $task_phid = $task->getPHID(); $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_ref_task) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => array($task_phid => $task_phid))); } } } break; case PhabricatorTransactions::TYPE_COMMENT: // When a user leaves a comment, upgrade their reviewer status from // "added" to "commented" if they're also a reviewer. We may further // upgrade this based on other actions in the transaction group. $status_added = DifferentialReviewerStatus::STATUS_ADDED; $status_commented = DifferentialReviewerStatus::STATUS_COMMENTED; $data = array( 'status' => $status_commented, ); $edits = array(); foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->getReviewerPHID() == $actor_phid) { if ($reviewer->getStatus() == $status_added) { $edits[$actor_phid] = array( 'data' => $data, ); } } } if ($edits) { $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => $edits)); } break; case DifferentialTransaction::TYPE_ACTION: $action_type = $xaction->getNewValue(); switch ($action_type) { case DifferentialAction::ACTION_ACCEPT: case DifferentialAction::ACTION_REJECT: if ($action_type == DifferentialAction::ACTION_ACCEPT) { $data = array( 'status' => DifferentialReviewerStatus::STATUS_ACCEPTED, ); } else { $data = array( 'status' => DifferentialReviewerStatus::STATUS_REJECTED, ); } $edits = array(); foreach ($object->getReviewerStatus() as $reviewer) { if ($reviewer->hasAuthority($actor)) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => $data, ); } } // Also either update or add the actor themselves as a reviewer. $edits[$actor_phid] = array( 'data' => $data, ); $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => $edits)); break; case DifferentialAction::ACTION_CLAIM: // If the user is commandeering, add the previous owner as a // reviewer and remove the actor. $edits = array( '-' => array( $actor_phid => $actor_phid, ), ); $owner_phid = $object->getAuthorPHID(); if ($owner_phid) { $reviewer = new DifferentialReviewer( $owner_phid, array( 'status' => DifferentialReviewerStatus::STATUS_ADDED, )); $edits['+'] = array( $owner_phid => array( 'data' => $reviewer->getEdgeData(), ), ); } // NOTE: We're setting setIsCommandeerSideEffect() on this because // normally you can't add a revision's author as a reviewer, but // this action swaps them after validation executes. $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setIsCommandeerSideEffect(true) ->setNewValue($edits); break; case DifferentialAction::ACTION_RESIGN: // If the user is resigning, add a separate reviewer edit // transaction which removes them as a reviewer. $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_reviewer) ->setIgnoreOnNoEffect(true) ->setNewValue( array( '-' => array( $actor_phid => $actor_phid, ), )); break; } break; } if (!$this->didExpandInlineState) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case DifferentialTransaction::TYPE_ACTION: case DifferentialTransaction::TYPE_UPDATE: case DifferentialTransaction::TYPE_INLINE: $this->didExpandInlineState = true; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID() == $actor_phid); if (!$actor_is_author) { break; } $state_map = PhabricatorTransactions::getInlineStateMap(); $inlines = id(new DifferentialDiffInlineCommentQuery()) ->setViewer($this->getActor()) ->withRevisionPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)) ->execute(); if (!$inlines) { break; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $results[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); break; } } return $results; } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_ACTION: return; case DifferentialTransaction::TYPE_INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; case DifferentialTransaction::TYPE_UPDATE: // Now that we're inside the transaction, do a final check. $diff = $this->requireDiff($xaction->getNewValue()); // TODO: It would be slightly cleaner to just revalidate this // transaction somehow using the same validation code, but that's // not easy to do at the moment. $revision_id = $diff->getRevisionID(); if ($revision_id && ($revision_id != $object->getID())) { throw new Exception( pht( 'Diff is already attached to another revision. You lost '. 'a race?')); } // TODO: This can race with diff updates, particularly those from // Harbormaster. See discussion in T8650. $diff->setRevisionID($object->getID()); $diff->save(); // Update Harbormaster to set the containerPHID correctly for any // existing buildables. We may otherwise have buildables stuck with // the old (`null`) container. // TODO: This is a bit iffy, maybe we can find a cleaner approach? // In particular, this could (rarely) be overwritten by Harbormaster // workers. $table = new HarbormasterBuildable(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET containerPHID = %s WHERE buildablePHID = %s', $table->getTableName(), $object->getPHID(), $diff->getPHID()); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $table = new DifferentialTransactionComment(); $conn_w = $table->establishConnection('w'); foreach ($xaction->getNewValue() as $phid => $state) { queryfx( $conn_w, 'UPDATE %T SET fixedState = %s WHERE phid = %s', $table->getTableName(), $state, $phid); } break; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function mergeEdgeData($type, array $u, array $v) { $result = parent::mergeEdgeData($type, $u, $v); switch ($type) { case DifferentialRevisionHasReviewerEdgeType::EDGECONST: // When the same reviewer has their status updated by multiple // transactions, we want the strongest status to win. An example of // this is when a user adds a comment and also accepts a revision which // they are a reviewer on. The comment creates a "commented" status, // while the accept creates an "accepted" status. Since accept is // stronger, it should win and persist. $u_status = idx($u, 'status'); $v_status = idx($v, 'status'); $u_str = DifferentialReviewerStatus::getStatusStrength($u_status); $v_str = DifferentialReviewerStatus::getStatusStrength($v_status); if ($u_str > $v_str) { $result['status'] = $u_status; } else { $result['status'] = $v_status; } break; } return $result; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load the most up-to-date version of the revision and its reviewers, // so we don't need to try to deduce the state of reviewers by examining // all the changes made by the transactions. Then, update the reviewers // on the object to make sure we're acting on the current reviewer set // (and, for example, sending mail to the right people). $new_revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewerStatus(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); if (!$new_revision) { throw new Exception( pht('Failed to load revision from transaction finalization.')); } $object->attachReviewerStatus($new_revision->getReviewerStatus()); $object->attachActiveDiff($new_revision->getActiveDiff()); $object->attachRepository($new_revision->getRepository()); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: $diff = $this->requireDiff($xaction->getNewValue(), true); // Update these denormalized index tables when we attach a new // diff to a revision. $this->updateRevisionHashTable($object, $diff); $this->updateAffectedPathTable($object, $diff); break; } } $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $old_status = $object->getStatus(); switch ($old_status) { case $status_accepted: case $status_revision: case $status_review: // Try to move a revision to "accepted". We look for: // // - at least one accepting reviewer who is a user; and // - no rejects; and // - no rejects of older diffs; and // - no blocking reviewers. $has_accepting_user = false; $has_rejecting_reviewer = false; $has_rejecting_older_reviewer = false; $has_blocking_reviewer = false; foreach ($object->getReviewerStatus() as $reviewer) { $reviewer_status = $reviewer->getStatus(); switch ($reviewer_status) { case DifferentialReviewerStatus::STATUS_REJECTED: $has_rejecting_reviewer = true; break; case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: $has_rejecting_older_reviewer = true; break; case DifferentialReviewerStatus::STATUS_BLOCKING: $has_blocking_reviewer = true; break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($reviewer->isUser()) { $has_accepting_user = true; } break; } } $new_status = null; if ($has_accepting_user && !$has_rejecting_reviewer && !$has_rejecting_older_reviewer && !$has_blocking_reviewer) { $new_status = $status_accepted; } else if ($has_rejecting_reviewer) { // This isn't accepted, and there's at least one rejecting reviewer, // so the revision needs changes. This usually happens after a // "reject". $new_status = $status_revision; } else if ($old_status == $status_accepted) { // This revision was accepted, but it no longer satisfies the // conditions for acceptance. This usually happens after an accepting // reviewer resigns or is removed. $new_status = $status_review; } if ($new_status !== null && ($new_status != $old_status)) { $xaction = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_STATUS) ->setOldValue($old_status) ->setNewValue($new_status); $xaction = $this->populateTransaction($object, $xaction)->save(); $xactions[] = $xaction; $object->setStatus($new_status)->save(); } break; default: // Revisions can't transition out of other statuses (like closed or // abandoned) as a side effect of reviewer status changes. break; } return $xactions; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); $config_self_accept_key = 'differential.allow-self-accept'; $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key); foreach ($xactions as $xaction) { switch ($type) { case PhabricatorTransactions::TYPE_EDGE: switch ($xaction->getMetadataValue('edge:type')) { case DifferentialRevisionHasReviewerEdgeType::EDGECONST: // Prevent the author from becoming a reviewer. // NOTE: This is pretty gross, but this restriction is unusual. // If we end up with too much more of this, we should try to clean // this up -- maybe by moving validation to after transactions // are adjusted (so we can just examine the final value) or adding // a second phase there? $author_phid = $object->getAuthorPHID(); $new = $xaction->getNewValue(); $add = idx($new, '+', array()); $eq = idx($new, '=', array()); $phids = array_keys($add + $eq); foreach ($phids as $phid) { if (($phid == $author_phid) && !$allow_self_accept && !$xaction->getIsCommandeerSideEffect()) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('The author of a revision can not be a reviewer.'), $xaction); } } break; } break; case DifferentialTransaction::TYPE_UPDATE: $diff = $this->loadDiff($xaction->getNewValue()); if (!$diff) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('The specified diff does not exist.'), $xaction); } else if (($diff->getRevisionID()) && ($diff->getRevisionID() != $object->getID())) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'You can not update this revision to the specified diff, '. 'because the diff is already attached to another revision.'), $xaction); } break; case DifferentialTransaction::TYPE_ACTION: $error = $this->validateDifferentialAction( $object, $type, $xaction, $xaction->getNewValue()); if ($error) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $error, $xaction); } break; } } return $errors; } private function validateDifferentialAction( DifferentialRevision $revision, $type, DifferentialTransaction $xaction, $action) { $author_phid = $revision->getAuthorPHID(); $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($author_phid == $actor_phid); $config_abandon_key = 'differential.always-allow-abandon'; $always_allow_abandon = PhabricatorEnv::getEnvConfig($config_abandon_key); $config_close_key = 'differential.always-allow-close'; $always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key); $config_reopen_key = 'differential.allow-reopen'; $allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key); $config_self_accept_key = 'differential.allow-self-accept'; $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key); $revision_status = $revision->getStatus(); $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; switch ($action) { case DifferentialAction::ACTION_ACCEPT: if ($actor_is_author && !$allow_self_accept) { return pht( 'You can not accept this revision because you are the owner.'); } if ($revision_status == $status_abandoned) { return pht( 'You can not accept this revision because it has been '. 'abandoned.'); } if ($revision_status == $status_closed) { return pht( 'You can not accept this revision because it has already been '. 'closed.'); } // TODO: It would be nice to make this generic at some point. $signatures = DifferentialRequiredSignaturesField::loadForRevision( $revision); foreach ($signatures as $phid => $signed) { if (!$signed) { return pht( 'You can not accept this revision because the author has '. 'not signed all of the required legal documents.'); } } break; case DifferentialAction::ACTION_REJECT: if ($actor_is_author) { return pht('You can not request changes to your own revision.'); } if ($revision_status == $status_abandoned) { return pht( 'You can not request changes to this revision because it has been '. 'abandoned.'); } if ($revision_status == $status_closed) { return pht( 'You can not request changes to this revision because it has '. 'already been closed.'); } break; case DifferentialAction::ACTION_RESIGN: // You can always resign from a revision if you're a reviewer. If you // aren't, this is a no-op rather than invalid. break; case DifferentialAction::ACTION_CLAIM: // You can claim a revision if you're not the owner. If you are, this // is a no-op rather than invalid. if ($revision_status == $status_closed) { return pht( 'You can not commandeer this revision because it has already been '. 'closed.'); } break; case DifferentialAction::ACTION_ABANDON: if (!$actor_is_author && !$always_allow_abandon) { return pht( 'You can not abandon this revision because you do not own it. '. 'You can only abandon revisions you own.'); } if ($revision_status == $status_closed) { return pht( 'You can not abandon this revision because it has already been '. 'closed.'); } // NOTE: Abandons of already-abandoned revisions are treated as no-op // instead of invalid. Other abandons are OK. break; case DifferentialAction::ACTION_RECLAIM: if (!$actor_is_author) { return pht( 'You can not reclaim this revision because you do not own '. 'it. You can only reclaim revisions you own.'); } if ($revision_status == $status_closed) { return pht( 'You can not reclaim this revision because it has already been '. 'closed.'); } // NOTE: Reclaims of other non-abandoned revisions are treated as no-op // instead of invalid. break; case DifferentialAction::ACTION_REOPEN: if (!$allow_reopen) { return pht( 'The reopen action is not enabled on this Phabricator install. '. 'Adjust your configuration to enable it.'); } // NOTE: If the revision is not closed, this is caught as a no-op // instead of an invalid transaction. break; case DifferentialAction::ACTION_RETHINK: if (!$actor_is_author) { return pht( 'You can not plan changes to this revision because you do not '. 'own it. To plan changes to a revision, you must be its owner.'); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: // These are OK. break; case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: // Let this through, it's a no-op. break; case ArcanistDifferentialRevisionStatus::ABANDONED: return pht( 'You can not plan changes to this revision because it has '. 'been abandoned.'); case ArcanistDifferentialRevisionStatus::CLOSED: return pht( 'You can not plan changes to this revision because it has '. 'already been closed.'); default: throw new Exception( pht( 'Encountered unexpected revision status ("%s") when '. 'validating "%s" action.', $revision_status, $action)); } break; case DifferentialAction::ACTION_REQUEST: if (!$actor_is_author) { return pht( 'You can not request review of this revision because you do '. 'not own it. To request review of a revision, you must be its '. 'owner.'); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: // These are OK. break; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: // This will be caught as "no effect" later on. break; case ArcanistDifferentialRevisionStatus::ABANDONED: return pht( 'You can not request review of this revision because it has '. 'been abandoned. Instead, reclaim it.'); case ArcanistDifferentialRevisionStatus::CLOSED: return pht( 'You can not request review of this revision because it has '. 'already been closed.'); default: throw new Exception( pht( 'Encountered unexpected revision status ("%s") when '. 'validating "%s" action.', $revision_status, $action)); } break; case DifferentialAction::ACTION_CLOSE: // We force revisions closed when we discover a corresponding commit. // In this case, revisions are allowed to transition to closed from // any state. This is an automated action taken by the daemons. if (!$this->getIsCloseByCommit()) { if (!$actor_is_author && !$always_allow_close) { return pht( 'You can not close this revision because you do not own it. To '. 'close a revision, you must be its owner.'); } if ($revision_status != $status_accepted) { return pht( 'You can not close this revision because it has not been '. 'accepted. You can only close accepted revisions.'); } } break; } return null; } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == DifferentialTransaction::TYPE_INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) {} return parent::requireCapabilities($object, $xaction); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); $phids[] = $object->getAuthorPHID(); foreach ($object->getReviewerStatus() as $reviewer) { $phids[] = $reviewer->getReviewerPHID(); } return $phids; } protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { $action = parent::getMailAction($object, $xactions); $strongest = $this->getStrongestAction($object, $xactions); switch ($strongest->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: $count = new PhutilNumber($object->getLineCount()); $action = pht('%s, %s line(s)', $action, $count); break; } return $action; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // This is nonstandard, but retains threading with older messages. $phid = $object->getPHID(); return "differential-rev-{$phid}-req"; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new DifferentialReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); $original_title = $object->getOriginalTitle(); $subject = "D{$id}: {$title}"; $thread_topic = "D{$id}: {$original_title}"; return id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addHeader('Thread-Topic', $thread_topic); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = new PhabricatorMetaMTAMailBody(); $body->setViewer($this->requireActor()); $this->addHeadersAndCommentsToMailBody($body, $xactions); $type_inline = DifferentialTransaction::TYPE_INLINE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } $changed_uri = $this->getChangedPriorToCommitURI(); if ($changed_uri) { $body->addLinkSection( pht('CHANGED PRIOR TO COMMIT'), $changed_uri); } $this->addCustomFieldsToMailBody($body, $object, $xactions); $body->addLinkSection( pht('REVISION DETAIL'), PhabricatorEnv::getProductionURI('/D'.$object->getID())); $update_xaction = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: $update_xaction = $xaction; break; } } if ($update_xaction) { $diff = $this->requireDiff($update_xaction->getNewValue(), true); $body->addTextSection( pht('AFFECTED FILES'), $this->renderAffectedFilesForMail($diff)); $config_key_inline = 'metamta.differential.inline-patches'; $config_inline = PhabricatorEnv::getEnvConfig($config_key_inline); $config_key_attach = 'metamta.differential.attach-patches'; $config_attach = PhabricatorEnv::getEnvConfig($config_key_attach); if ($config_inline || $config_attach) { $patch_section = $this->renderPatchForMail($diff); $lines = count(phutil_split_lines($patch_section->getPlaintext())); if ($config_inline && ($lines <= $config_inline)) { $body->addTextSection( pht('CHANGE DETAILS'), $patch_section); } if ($config_attach) { $name = pht('D%s.%s.patch', $object->getID(), $diff->getID()); $mime_type = 'text/x-patch; charset=utf-8'; $body->addAttachment( new PhabricatorMetaMTAAttachment( $patch_section->getPlaintext(), $name, $mime_type)); } } } return $body; } public function getMailTagsMap() { return array( DifferentialTransaction::MAILTAG_REVIEW_REQUEST => pht('A revision is created.'), DifferentialTransaction::MAILTAG_UPDATED => pht('A revision is updated.'), DifferentialTransaction::MAILTAG_COMMENT => pht('Someone comments on a revision.'), DifferentialTransaction::MAILTAG_CLOSED => pht('A revision is closed.'), DifferentialTransaction::MAILTAG_REVIEWERS => pht("A revision's reviewers change."), DifferentialTransaction::MAILTAG_CC => pht("A revision's CCs change."), DifferentialTransaction::MAILTAG_OTHER => pht('Other revision activity not listed above occurs.'), ); } protected function supportsSearch() { return true; } protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) {} return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, $blocks, PhutilMarkupEngine $engine) { $flat_blocks = array_mergev($blocks); $huge_block = implode("\n\n", $flat_blocks); $task_map = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($huge_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $task_id = (int)trim($monogram, 'tT'); $task_map[$task_id] = true; } } $rev_map = array(); $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($huge_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $rev_id = (int)trim($monogram, 'dD'); $rev_map[$rev_id] = true; } } $edges = array(); $task_phids = array(); $rev_phids = array(); if ($task_map) { $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($task_map)) ->execute(); if ($tasks) { $task_phids = mpull($tasks, 'getPHID', 'getPHID'); $edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST; $edges[$edge_related] = $task_phids; } } if ($rev_map) { $revs = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($rev_map)) ->execute(); $rev_phids = mpull($revs, 'getPHID', 'getPHID'); // NOTE: Skip any write attempts if a user cleverly implies a revision // depends upon itself. unset($rev_phids[$object->getPHID()]); if ($revs) { $depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; $edges[$depends] = $rev_phids; } } $this->setUnmentionablePHIDMap(array_merge($task_phids, $rev_phids)); $result = array(); foreach ($edges as $type => $specs) { $result[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type) ->setNewValue(array('+' => $specs)); } return $result; } protected function indentForMail(array $lines) { $indented = array(); foreach ($lines as $line) { $indented[] = '> '.$line; } return $indented; } protected function nestCommentHistory( DifferentialTransactionComment $comment, array $comments_by_line_number, array $users_by_phid) { $nested = array(); $previous_comments = $comments_by_line_number[$comment->getChangesetID()] [$comment->getLineNumber()]; foreach ($previous_comments as $previous_comment) { if ($previous_comment->getID() >= $comment->getID()) { break; } $nested = $this->indentForMail( array_merge( $nested, explode("\n", $previous_comment->getContent()))); $user = idx($users_by_phid, $previous_comment->getAuthorPHID(), null); if ($user) { array_unshift($nested, pht('%s wrote:', $user->getUserName())); } } $nested = array_merge($nested, explode("\n", $comment->getContent())); return implode("\n", $nested); } private function renderInlineCommentsForMail( PhabricatorLiskDAO $object, array $inlines) { $context_key = 'metamta.differential.unified-comment-context'; $show_context = PhabricatorEnv::getEnvConfig($context_key); $changeset_ids = array(); $line_numbers_by_changeset = array(); foreach ($inlines as $inline) { $id = $inline->getComment()->getChangesetID(); $changeset_ids[$id] = $id; $line_numbers_by_changeset[$id][] = $inline->getComment()->getLineNumber(); } $changesets = id(new DifferentialChangesetQuery()) ->setViewer($this->getActor()) ->withIDs($changeset_ids) ->needHunks(true) ->execute(); $inline_groups = DifferentialTransactionComment::sortAndGroupInlines( $inlines, $changesets); if ($show_context) { $hunk_parser = new DifferentialHunkParser(); $table = new DifferentialTransactionComment(); $conn_r = $table->establishConnection('r'); $queries = array(); foreach ($line_numbers_by_changeset as $id => $line_numbers) { $queries[] = qsprintf( $conn_r, '(changesetID = %d AND lineNumber IN (%Ld))', $id, $line_numbers); } $all_comments = id(new DifferentialTransactionComment())->loadAllWhere( 'transactionPHID IS NOT NULL AND (%Q)', implode(' OR ', $queries)); $comments_by_line_number = array(); foreach ($all_comments as $comment) { $comments_by_line_number [$comment->getChangesetID()] [$comment->getLineNumber()] [$comment->getID()] = $comment; } $author_phids = mpull($all_comments, 'getAuthorPHID'); $authors = id(new PhabricatorPeopleQuery()) ->setViewer($this->getActor()) ->withPHIDs($author_phids) ->execute(); $authors_by_phid = mpull($authors, null, 'getPHID'); } $section = new PhabricatorMetaMTAMailSection(); foreach ($inline_groups as $changeset_id => $group) { $changeset = idx($changesets, $changeset_id); if (!$changeset) { continue; } foreach ($group as $inline) { $comment = $inline->getComment(); $file = $changeset->getFilename(); $start = $comment->getLineNumber(); $len = $comment->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $inline_content = $comment->getContent(); if (!$show_context) { $section->addFragment("{$file}:{$range} {$inline_content}"); } else { $patch = $hunk_parser->makeContextDiff( $changeset->getHunks(), $comment->getIsNewFile(), $comment->getLineNumber(), $comment->getLineLength(), 1); $nested_comments = $this->nestCommentHistory( $inline->getComment(), $comments_by_line_number, $authors_by_phid); $section ->addFragment('================') ->addFragment(pht('Comment at: %s:%s', $file, $range)) ->addPlaintextFragment($patch) ->addHTMLFragment($this->renderPatchHTMLForMail($patch)) ->addFragment('----------------') ->addFragment($nested_comments) ->addFragment(null); } } } return $section; } private function loadDiff($phid, $need_changesets = false) { $query = id(new DifferentialDiffQuery()) ->withPHIDs(array($phid)) ->setViewer($this->getActor()); if ($need_changesets) { $query->needChangesets(true); } return $query->executeOne(); } private function requireDiff($phid, $need_changesets = false) { $diff = $this->loadDiff($phid, $need_changesets); if (!$diff) { throw new Exception(pht('Diff "%s" does not exist!', $phid)); } return $diff; } /* -( Herald Integration )------------------------------------------------- */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { if ($this->getIsNewObject()) { return true; } foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_UPDATE: if (!$this->getIsCloseByCommit()) { return true; } break; case DifferentialTransaction::TYPE_ACTION: switch ($xaction->getNewValue()) { case DifferentialAction::ACTION_CLAIM: // When users commandeer revisions, we may need to trigger // signatures or author-based rules. return true; } break; } } return parent::shouldApplyHeraldRules($object, $xactions); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($object->getPHID())) ->needActiveDiffs(true) ->needReviewerStatus(true) ->executeOne(); if (!$revision) { throw new Exception( pht('Failed to load revision for Herald adapter construction!')); } $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $revision, $revision->getActiveDiff()); - $reviewers = $revision->getReviewerStatus(); - $reviewer_phids = mpull($reviewers, 'getReviewerPHID'); - - $adapter->setExplicitReviewers($reviewer_phids); - return $adapter; } - protected function didApplyHeraldRules( - PhabricatorLiskDAO $object, - HeraldAdapter $adapter, - HeraldTranscript $transcript) { - - $xactions = array(); - - // Build a transaction to adjust reviewers. - $reviewers = array( - DifferentialReviewerStatus::STATUS_ADDED => - array_keys($adapter->getReviewersAddedByHerald()), - DifferentialReviewerStatus::STATUS_BLOCKING => - array_keys($adapter->getBlockingReviewersAddedByHerald()), - ); - - $old_reviewers = $object->getReviewerStatus(); - $old_reviewers = mpull($old_reviewers, null, 'getReviewerPHID'); - - $value = array(); - foreach ($reviewers as $status => $phids) { - foreach ($phids as $phid) { - if ($phid == $object->getAuthorPHID()) { - // Don't try to add the revision's author as a reviewer, since this - // isn't valid and doesn't make sense. - continue; - } - - // If the target is already a reviewer, don't try to change anything - // if their current status is at least as strong as the new status. - // For example, don't downgrade an "Accepted" to a "Blocking Reviewer". - $old_reviewer = idx($old_reviewers, $phid); - if ($old_reviewer) { - $old_status = $old_reviewer->getStatus(); - - $old_strength = DifferentialReviewerStatus::getStatusStrength( - $old_status); - $new_strength = DifferentialReviewerStatus::getStatusStrength( - $status); - - if ($new_strength <= $old_strength) { - continue; - } - } - - $value['+'][$phid] = array( - 'data' => array( - 'status' => $status, - ), - ); - } - } - - if ($value) { - $edge_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; - - $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $edge_reviewer) - ->setNewValue($value); - } - - // Require legalpad document signatures. - $legal_phids = $adapter->getRequiredSignatureDocumentPHIDs(); - if ($legal_phids) { - // We only require signatures of documents which have not already - // been signed. In general, this reduces the amount of churn that - // signature rules cause. - - $signatures = id(new LegalpadDocumentSignatureQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withDocumentPHIDs($legal_phids) - ->withSignerPHIDs(array($object->getAuthorPHID())) - ->execute(); - $signed_phids = mpull($signatures, 'getDocumentPHID'); - $legal_phids = array_diff($legal_phids, $signed_phids); - - // If we still have something to trigger, add the edges. - if ($legal_phids) { - $edge_legal = LegalpadObjectNeedsSignatureEdgeType::EDGECONST; - $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $edge_legal) - ->setNewValue( - array( - '+' => array_fuse($legal_phids), - )); - } - } - - // Apply build plans. - HarbormasterBuildable::applyBuildPlans( - $adapter->getDiff()->getPHID(), - $adapter->getPHID(), - $adapter->getBuildPlans()); - - return $xactions; - } - /** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable( DifferentialRevision $revision, DifferentialDiff $diff) { $repository = $revision->getRepository(); if (!$repository) { // The repository where the code lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $changesets = $diff->getChangesets(); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix.'/'.$changeset->getFilename(); } // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_ids = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( $all_paths); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($path_ids as $path_id) { $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } } /** * Update the table connecting revisions to DVCS local hashes, so we can * identify revisions by commit/tree hashes. */ private function updateRevisionHashTable( DifferentialRevision $revision, DifferentialDiff $diff) { $vcs = $diff->getSourceControlSystem(); if ($vcs == DifferentialRevisionControlSystem::SVN) { // Subversion has no local commit or tree hash information, so we don't // have to do anything. return; } $property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff->getID(), 'local:commits'); if (!$property) { return; } $hashes = array(); $data = $property->getData(); switch ($vcs) { case DifferentialRevisionControlSystem::GIT: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, $commit['commit'], ); $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_TREE, $commit['tree'], ); } break; case DifferentialRevisionControlSystem::MERCURIAL: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, $commit['rev'], ); } break; } $conn_w = $revision->establishConnection('w'); $sql = array(); foreach ($hashes as $info) { list($type, $hash) = $info; $sql[] = qsprintf( $conn_w, '(%d, %s, %s)', $revision->getID(), $type, $hash); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', ArcanistDifferentialRevisionHash::TABLE_NAME, $revision->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', ArcanistDifferentialRevisionHash::TABLE_NAME, implode(', ', $sql)); } } private function renderAffectedFilesForMail(DifferentialDiff $diff) { $changesets = $diff->getChangesets(); $filenames = mpull($changesets, 'getDisplayFilename'); sort($filenames); $count = count($filenames); $max = 250; if ($count > $max) { $filenames = array_slice($filenames, 0, $max); $filenames[] = pht('(%d more files...)', ($count - $max)); } return implode("\n", $filenames); } private function renderPatchHTMLForMail($patch) { return phutil_tag('pre', array('style' => 'font-family: monospace;'), $patch); } private function renderPatchForMail(DifferentialDiff $diff) { $format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format'); $patch = id(new DifferentialRawDiffRenderer()) ->setViewer($this->getActor()) ->setFormat($format) ->setChangesets($diff->getChangesets()) ->buildPatch(); $section = new PhabricatorMetaMTAMailSection(); $section->addHTMLFragment($this->renderPatchHTMLForMail($patch)); $section->addPlaintextFragment($patch); return $section; } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { // Reload to pick up the active diff and reviewer status. return id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewerStatus(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); } protected function getCustomWorkerState() { return array( 'changedPriorToCommitURI' => $this->changedPriorToCommitURI, ); } protected function loadCustomWorkerState(array $state) { $this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI'); return $this; } } diff --git a/src/applications/differential/herald/DifferentialBlockHeraldAction.php b/src/applications/differential/herald/DifferentialBlockHeraldAction.php new file mode 100644 index 0000000000..33ad4a3480 --- /dev/null +++ b/src/applications/differential/herald/DifferentialBlockHeraldAction.php @@ -0,0 +1,56 @@ +logEffect(self::DO_BLOCK); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_TEXT; + } + + public function renderActionDescription($value) { + return pht('Block diff with message: %s', $value); + } + + protected function getActionEffectMap() { + return array( + self::DO_BLOCK => array( + 'icon' => 'fa-stop', + 'color' => 'red', + 'name' => pht('Blocked Diff'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_BLOCK: + return pht('Blocked diff.'); + } + } +} diff --git a/src/applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php new file mode 100644 index 0000000000..79a00bc940 --- /dev/null +++ b/src/applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php @@ -0,0 +1,32 @@ +applyReviewers($effect->getTarget(), $is_blocking = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorMetaMTAMailableDatasource(); + } + + public function renderActionDescription($value) { + return pht('Add blocking reviewers: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php new file mode 100644 index 0000000000..938f298955 --- /dev/null +++ b/src/applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php @@ -0,0 +1,29 @@ +getRule()->getAuthorPHID(); + return $this->applyReviewers(array($phid), $is_blocking = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Add rule author as blocking reviewer.'); + } + +} diff --git a/src/applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php new file mode 100644 index 0000000000..45f209385e --- /dev/null +++ b/src/applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php @@ -0,0 +1,32 @@ +applyReviewers($effect->getTarget(), $is_blocking = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorMetaMTAMailableDatasource(); + } + + public function renderActionDescription($value) { + return pht('Add reviewers: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php new file mode 100644 index 0000000000..a7e9991901 --- /dev/null +++ b/src/applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php @@ -0,0 +1,29 @@ +getRule()->getAuthorPHID(); + return $this->applyReviewers(array($phid), $is_blocking = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Add rule author as reviewer.'); + } + +} diff --git a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php new file mode 100644 index 0000000000..c568537f7c --- /dev/null +++ b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php @@ -0,0 +1,149 @@ +getAdapter(); + $object = $adapter->getObject(); + + $phids = array_fuse($phids); + + // Don't try to add revision authors as reviewers. + $authors = array(); + foreach ($phids as $phid) { + if ($phid == $object->getAuthorPHID()) { + $authors[] = $phid; + unset($phids[$phid]); + } + } + + if ($authors) { + $this->logEffect(self::DO_AUTHORS, $authors); + if (!$phids) { + return; + } + } + + $reviewers = $object->getReviewerStatus(); + $reviewers = mpull($reviewers, null, 'getReviewerPHID'); + + if ($is_blocking) { + $new_status = DifferentialReviewerStatus::STATUS_BLOCKING; + } else { + $new_status = DifferentialReviewerStatus::STATUS_ADDED; + } + + $new_strength = DifferentialReviewerStatus::getStatusStrength( + $new_status); + + $current = array(); + foreach ($phids as $phid) { + if (!isset($reviewers[$phid])) { + continue; + } + + // If we're applying a stronger status (usually, upgrading a reviewer + // into a blocking reviewer), skip this check so we apply the change. + $old_strength = DifferentialReviewerStatus::getStatusStrength( + $reviewers[$phid]->getStatus()); + if ($old_strength <= $new_strength) { + continue; + } + + $current[] = $phid; + } + + $allowed_types = array( + PhabricatorPeopleUserPHIDType::TYPECONST, + PhabricatorProjectProjectPHIDType::TYPECONST, + ); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + $value = array(); + foreach ($phids as $phid) { + $value[$phid] = array( + 'data' => array( + 'status' => $new_status, + ), + ); + } + + $edgetype_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; + + $xaction = $adapter->newTransaction() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edgetype_reviewer) + ->setNewValue( + array( + '+' => $value, + )); + + $adapter->queueTransaction($xaction); + + if ($is_blocking) { + $this->logEffect(self::DO_ADD_BLOCKING_REVIEWERS, $phids); + } else { + $this->logEffect(self::DO_ADD_REVIEWERS, $phids); + } + } + + protected function getActionEffectMap() { + return array( + self::DO_AUTHORS => array( + 'icon' => 'fa-user', + 'color' => 'grey', + 'name' => pht('Revision Author'), + ), + self::DO_ADD_REVIEWERS => array( + 'icon' => 'fa-user', + 'color' => 'green', + 'name' => pht('Added Reviewers'), + ), + self::DO_ADD_BLOCKING_REVIEWERS => array( + 'icon' => 'fa-user', + 'color' => 'green', + 'name' => pht('Added Blocking Reviewers'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_AUTHORS: + return pht( + 'Declined to add revision author as reviewer: %s.', + $this->renderHandleList($data)); + case self::DO_ADD_REVIEWERS: + return pht( + 'Added %s reviewer(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_ADD_BLOCKING_REVIEWERS: + return pht( + 'Added %s blocking reviewer(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + } + } + + +} diff --git a/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php b/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php index 225ba5b73d..9528978050 100644 --- a/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialDiffAdapter.php @@ -1,106 +1,66 @@ setDiff(new DifferentialDiff()); } public function isSingleEventAdapter() { return true; } protected function loadChangesets() { return $this->loadChangesetsWithHunks(); } protected function loadChangesetsWithHunks() { return $this->getDiff()->getChangesets(); } public function getObject() { return $this->getDiff(); } public function getAdapterContentType() { return 'differential.diff'; } public function getAdapterContentName() { return pht('Differential Diffs'); } public function getAdapterContentDescription() { return pht( "React to new diffs being uploaded, before writes occur.\n". "These rules can reject diffs before they are written to permanent ". "storage, to prevent users from accidentally uploading private keys or ". "other sensitive information."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: default: return false; } } public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::FIRST, ); } public function getHeraldName() { return pht('New Diff'); } - public function getActionNameMap($rule_type) { - return array( - self::ACTION_BLOCK => pht('Block diff with message'), - ) + parent::getActionNameMap($rule_type); - } - - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - return array_merge( - array( - self::ACTION_BLOCK, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } - } - - public function applyHeraldEffects(array $effects) { - assert_instances_of($effects, 'HeraldEffect'); - - $result = array(); - foreach ($effects as $effect) { - $action = $effect->getAction(); - switch ($action) { - case self::ACTION_BLOCK: - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Blocked diff.')); - break; - default: - $result[] = $this->applyStandardEffect($effect); - break; - } - } - - return $result; - } - } diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php index d3264860d6..a47d433357 100644 --- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php @@ -1,244 +1,150 @@ revision = $this->newObject(); } public function getObject() { return $this->revision; } public function getAdapterContentType() { return 'differential'; } public function getAdapterContentName() { return pht('Differential Revisions'); } public function getAdapterContentDescription() { return pht( "React to revisions being created or updated.\n". "Revision rules can send email, flag revisions, add reviewers, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, HeraldRepetitionPolicyConfig::FIRST, ); } public static function newLegacyAdapter( DifferentialRevision $revision, DifferentialDiff $diff) { $object = new HeraldDifferentialRevisionAdapter(); // Reload the revision to pick up relationship information. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision->getID())) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); $object->revision = $revision; $object->setDiff($diff); return $object; } - public function setExplicitReviewers($explicit_reviewers) { - $this->explicitReviewers = $explicit_reviewers; - return $this; - } - - public function getReviewersAddedByHerald() { - return $this->addReviewerPHIDs; - } - - public function getBlockingReviewersAddedByHerald() { - return $this->blockingReviewerPHIDs; - } - - public function getRequiredSignatureDocumentPHIDs() { - return $this->requiredSignatureDocumentPHIDs; - } - - public function getBuildPlans() { - return $this->buildPlans; - } - public function getHeraldName() { return $this->revision->getTitle(); } protected function loadChangesets() { if ($this->changesets === null) { $this->changesets = $this->getDiff()->loadChangesets(); } return $this->changesets; } protected function loadChangesetsWithHunks() { $changesets = $this->loadChangesets(); if ($changesets && !$this->haveHunks) { $this->haveHunks = true; id(new DifferentialHunkQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withChangesets($changesets) ->needAttachToChangesets(true) ->execute(); } return $changesets; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $this->affectedPackages = array(); $repository = $this->loadRepository(); if ($repository) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } } return $this->affectedPackages; } public function loadReviewers() { - // TODO: This can probably go away as I believe it's just a performance - // optimization, just retaining it while modularizing fields to limit the - // scope of that change. - if (isset($this->explicitReviewers)) { - return array_keys($this->explicitReviewers); - } else { - return $this->revision->getReviewers(); - } + $reviewers = $this->getObject()->getReviewerStatus(); + return mpull($reviewers, 'getReviewerPHID'); } - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_ADD_REVIEWERS, - self::ACTION_ADD_BLOCKING_REVIEWERS, - self::ACTION_APPLY_BUILD_PLANS, - self::ACTION_REQUIRE_SIGNATURE, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_FLAG, - self::ACTION_ADD_REVIEWERS, - self::ACTION_ADD_BLOCKING_REVIEWERS, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } + +/* -( HarbormasterBuildableAdapterInterface )------------------------------ */ + + + public function getHarbormasterBuildablePHID() { + return $this->getDiff()->getPHID(); } - public function applyHeraldEffects(array $effects) { - assert_instances_of($effects, 'HeraldEffect'); - - $result = array(); - - foreach ($effects as $effect) { - $action = $effect->getAction(); - switch ($action) { - case self::ACTION_ADD_REVIEWERS: - foreach ($effect->getTarget() as $phid) { - $this->addReviewerPHIDs[$phid] = true; - } - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Added reviewers.')); - break; - case self::ACTION_ADD_BLOCKING_REVIEWERS: - // This adds reviewers normally, it just also marks them blocking. - foreach ($effect->getTarget() as $phid) { - $this->addReviewerPHIDs[$phid] = true; - $this->blockingReviewerPHIDs[$phid] = true; - } - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Added blocking reviewers.')); - break; - case self::ACTION_APPLY_BUILD_PLANS: - foreach ($effect->getTarget() as $phid) { - $this->buildPlans[] = $phid; - } - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Applied build plans.')); - break; - case self::ACTION_REQUIRE_SIGNATURE: - foreach ($effect->getTarget() as $phid) { - $this->requiredSignatureDocumentPHIDs[] = $phid; - } - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Required signatures.')); - break; - default: - $result[] = $this->applyStandardEffect($effect); - break; - } - } - return $result; + public function getHarbormasterContainerPHID() { + return $this->getObject()->getPHID(); + } + + public function getQueuedHarbormasterBuildPlanPHIDs() { + return $this->buildPlanPHIDs; + } + + public function queueHarbormasterBuildPlanPHID($phid) { + $this->buildPlanPHIDs[] = $phid; } } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index f84eb13bdf..ee257ebee0 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -1,1261 +1,1263 @@ remoteProtocol = $remote_protocol; return $this; } public function getRemoteProtocol() { return $this->remoteProtocol; } public function setRemoteAddress($remote_address) { $this->remoteAddress = $remote_address; return $this; } public function getRemoteAddress() { return $this->remoteAddress; } private function getRemoteAddressForLog() { // If whatever we have here isn't a valid IPv4 address, just store `null`. // Older versions of PHP return `-1` on failure instead of `false`. $remote_address = $this->getRemoteAddress(); $remote_address = max(0, ip2long($remote_address)); $remote_address = nonempty($remote_address, null); return $remote_address; } public function setSubversionTransactionInfo($transaction, $repository) { $this->subversionTransaction = $transaction; $this->subversionRepository = $repository; return $this; } public function setStdin($stdin) { $this->stdin = $stdin; return $this; } public function getStdin() { return $this->stdin; } public function setOriginalArgv(array $original_argv) { $this->originalArgv = $original_argv; return $this; } public function getOriginalArgv() { return $this->originalArgv; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setMercurialHook($mercurial_hook) { $this->mercurialHook = $mercurial_hook; return $this; } public function getMercurialHook() { return $this->mercurialHook; } /* -( Hook Execution )----------------------------------------------------- */ public function execute() { $ref_updates = $this->findRefUpdates(); $all_updates = $ref_updates; $caught = null; try { try { $this->rejectDangerousChanges($ref_updates); } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting dangerous changes, flag everything that we've // seen as rejected so it's clear that none of it was accepted. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS; throw $ex; } $this->applyHeraldRefRules($ref_updates, $all_updates); $content_updates = $this->findContentUpdates($ref_updates); $all_updates = array_merge($all_updates, $content_updates); $this->applyHeraldContentRules($content_updates, $all_updates); // Run custom scripts in `hook.d/` directories. $this->applyCustomHooks($all_updates); // If we make it this far, we're accepting these changes. Mark all the // logs as accepted. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT; } catch (Exception $ex) { // We'll throw this again in a minute, but we want to save all the logs // first. $caught = $ex; } // Save all the logs no matter what the outcome was. $event = $this->newPushEvent(); $event->setRejectCode($this->rejectCode); $event->setRejectDetails($this->rejectDetails); $event->openTransaction(); $event->save(); foreach ($all_updates as $update) { $update->setPushEventPHID($event->getPHID()); $update->save(); } $event->saveTransaction(); if ($caught) { throw $caught; } // If this went through cleanly, detect pushes which are actually imports // of an existing repository rather than an addition of new commits. If // this push is importing a bunch of stuff, set the importing flag on // the repository. It will be cleared once we fully process everything. if ($this->isInitialImport($all_updates)) { $repository = $this->getRepository(); $repository->openTransaction(); $repository->beginReadLocking(); $repository = $repository->reload(); $repository->setDetail('importing', true); $repository->save(); $repository->endReadLocking(); $repository->saveTransaction(); } if ($this->emailPHIDs) { // If Herald rules triggered email to users, queue a worker to send the // mail. We do this out-of-process so that we block pushes as briefly // as possible. // (We do need to pull some commit info here because the commit objects // may not exist yet when this worker runs, which could be immediately.) PhabricatorWorker::scheduleTask( 'PhabricatorRepositoryPushMailWorker', array( 'eventPHID' => $event->getPHID(), 'emailPHIDs' => array_values($this->emailPHIDs), 'info' => $this->loadCommitInfoForWorker($all_updates), ), array( 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); } return 0; } private function findRefUpdates() { $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return $this->findGitRefUpdates(); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return $this->findMercurialRefUpdates(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->findSubversionRefUpdates(); default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } } private function rejectDangerousChanges(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); $repository = $this->getRepository(); if ($repository->shouldAllowDangerousChanges()) { return; } $flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; foreach ($ref_updates as $ref_update) { if (!$ref_update->hasChangeFlags($flag_dangerous)) { // This is not a dangerous change. continue; } // We either have a branch deletion or a non fast-forward branch update. // Format a message and reject the push. $message = pht( "DANGEROUS CHANGE: %s\n". "Dangerous change protection is enabled for this repository.\n". "Edit the repository configuration before making dangerous changes.", $ref_update->getDangerousChangeDescription()); throw new DiffusionCommitHookRejectException($message); } } private function findContentUpdates(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return $this->findGitContentUpdates($ref_updates); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return $this->findMercurialContentUpdates($ref_updates); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: return $this->findSubversionContentUpdates($ref_updates); default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } } /* -( Herald )------------------------------------------------------------- */ private function applyHeraldRefRules( array $ref_updates, array $all_updates) { $this->applyHeraldRules( $ref_updates, new HeraldPreCommitRefAdapter(), $all_updates); } private function applyHeraldContentRules( array $content_updates, array $all_updates) { $this->applyHeraldRules( $content_updates, new HeraldPreCommitContentAdapter(), $all_updates); } private function applyHeraldRules( array $updates, HeraldAdapter $adapter_template, array $all_updates) { if (!$updates) { return; } $adapter_template->setHookEngine($this); $engine = new HeraldEngine(); $rules = null; $blocking_effect = null; $blocked_update = null; foreach ($updates as $update) { $adapter = id(clone $adapter_template) ->setPushLog($update); if ($rules === null) { $rules = $engine->loadRulesForAdapter($adapter); } $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); // Store any PHIDs we want to send email to for later. foreach ($adapter->getEmailPHIDs() as $email_phid) { $this->emailPHIDs[$email_phid] = $email_phid; } + $block_action = DiffusionBlockHeraldAction::ACTIONCONST; + if ($blocking_effect === null) { foreach ($effects as $effect) { - if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { + if ($effect->getAction() == $block_action) { $blocking_effect = $effect; $blocked_update = $update; break; } } } } if ($blocking_effect) { $rule = $blocking_effect->getRule(); $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD; $this->rejectDetails = $rule->getPHID(); $message = $blocking_effect->getTarget(); if (!strlen($message)) { $message = pht('(None.)'); } $blocked_ref_name = coalesce( $blocked_update->getRefName(), $blocked_update->getRefNewShort()); $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name; throw new DiffusionCommitHookRejectException( pht( "This push was rejected by Herald push rule %s.\n". "Change: %s\n". " Rule: %s\n". "Reason: %s", $rule->getMonogram(), $blocked_name, $rule->getName(), $message)); } } public function loadViewerProjectPHIDsForHerald() { // This just caches the viewer's projects so we don't need to load them // over and over again when applying Herald rules. if ($this->heraldViewerProjects === null) { $this->heraldViewerProjects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs(array($this->getViewer()->getPHID())) ->execute(); } return mpull($this->heraldViewerProjects, 'getPHID'); } /* -( Git )---------------------------------------------------------------- */ private function findGitRefUpdates() { $ref_updates = array(); // First, parse stdin, which lists all the ref changes. The input looks // like this: // // $stdin = $this->getStdin(); $lines = phutil_split_lines($stdin, $retain_endings = false); foreach ($lines as $line) { $parts = explode(' ', $line, 3); if (count($parts) != 3) { throw new Exception(pht('Expected "old new ref", got "%s".', $line)); } $ref_old = $parts[0]; $ref_new = $parts[1]; $ref_raw = $parts[2]; if (preg_match('(^refs/heads/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; $ref_raw = substr($ref_raw, strlen('refs/heads/')); } else if (preg_match('(^refs/tags/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG; $ref_raw = substr($ref_raw, strlen('refs/tags/')); } else { throw new Exception( pht( "Unable to identify the reftype of '%s'. Rejecting push.", $ref_raw)); } $ref_update = $this->newPushLog() ->setRefType($ref_type) ->setRefName($ref_raw) ->setRefOld($ref_old) ->setRefNew($ref_new); $ref_updates[] = $ref_update; } $this->findGitMergeBases($ref_updates); $this->findGitChangeFlags($ref_updates); return $ref_updates; } private function findGitMergeBases(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); $futures = array(); foreach ($ref_updates as $key => $ref_update) { // If the old hash is "00000...", the ref is being created (either a new // branch, or a new tag). If the new hash is "00000...", the ref is being // deleted. If both are nonempty, the ref is being updated. For updates, // we'll figure out the `merge-base` of the old and new objects here. This // lets us reject non-FF changes cheaply; later, we'll figure out exactly // which commits are new. $ref_old = $ref_update->getRefOld(); $ref_new = $ref_update->getRefNew(); if (($ref_old === self::EMPTY_HASH) || ($ref_new === self::EMPTY_HASH)) { continue; } $futures[$key] = $this->getRepository()->getLocalCommandFuture( 'merge-base %s %s', $ref_old, $ref_new); } $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $key => $future) { // If 'old' and 'new' have no common ancestors (for example, a force push // which completely rewrites a ref), `git merge-base` will exit with // an error and no output. It would be nice to find a positive test // for this instead, but I couldn't immediately come up with one. See // T4224. Assume this means there are no ancestors. list($err, $stdout) = $future->resolve(); if ($err) { $merge_base = null; } else { $merge_base = rtrim($stdout, "\n"); } $ref_update = $ref_updates[$key]; $ref_update->setMergeBase($merge_base); } return $ref_updates; } private function findGitChangeFlags(array $ref_updates) { assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog'); foreach ($ref_updates as $key => $ref_update) { $ref_old = $ref_update->getRefOld(); $ref_new = $ref_update->getRefNew(); $ref_type = $ref_update->getRefType(); $ref_flags = 0; $dangerous = null; if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) { // This happens if you try to delete a tag or branch which does not // exist by pushing directly to the ref. Git will warn about it but // allow it. Just call it a delete, without flagging it as dangerous. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else if ($ref_old === self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else if ($ref_new === self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; $dangerous = pht( "The change you're attempting to push deletes the branch '%s'.", $ref_update->getRefName()); } } else { $merge_base = $ref_update->getMergeBase(); if ($merge_base == $ref_old) { // This is a fast-forward update to an existing branch. // These are safe. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; // For now, we don't consider deleting or moving tags to be a // "dangerous" update. It's way harder to get wrong and should be easy // to recover from once we have better logging. Only add the dangerous // flag if this ref is a branch. if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; $dangerous = pht( "The change you're attempting to push updates the branch '%s' ". "from '%s' to '%s', but this is not a fast-forward. Pushes ". "which rewrite published branch history are dangerous.", $ref_update->getRefName(), $ref_update->getRefOldShort(), $ref_update->getRefNewShort()); } } } $ref_update->setChangeFlags($ref_flags); if ($dangerous !== null) { $ref_update->attachDangerousChangeDescription($dangerous); } } return $ref_updates; } private function findGitContentUpdates(array $ref_updates) { $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; $futures = array(); foreach ($ref_updates as $key => $ref_update) { if ($ref_update->hasChangeFlags($flag_delete)) { // Deleting a branch or tag can never create any new commits. continue; } // NOTE: This piece of magic finds all new commits, by walking backward // from the new value to the value of *any* existing ref in the // repository. Particularly, this will cover the cases of a new branch, a // completely moved tag, etc. $futures[$key] = $this->getRepository()->getLocalCommandFuture( 'log --format=%s %s --not --all', '%H', $ref_update->getRefNew()); } $content_updates = array(); $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $key => $future) { list($stdout) = $future->resolvex(); if (!strlen(trim($stdout))) { // This change doesn't have any new commits. One common case of this // is creating a new tag which points at an existing commit. continue; } $commits = phutil_split_lines($stdout, $retain_newlines = false); // If we're looking at a branch, mark all of the new commits as on that // branch. It's only possible for these commits to be on updated branches, // since any other branch heads are necessarily behind them. $branch_name = null; $ref_update = $ref_updates[$key]; $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; if ($ref_update->getRefType() == $type_branch) { $branch_name = $ref_update->getRefName(); } foreach ($commits as $commit) { if ($branch_name) { $this->gitCommits[$commit][] = $branch_name; } $content_updates[$commit] = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($commit) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); } } return $content_updates; } /* -( Custom )------------------------------------------------------------- */ private function applyCustomHooks(array $updates) { $args = $this->getOriginalArgv(); $stdin = $this->getStdin(); $console = PhutilConsole::getConsole(); $env = array( 'PHABRICATOR_REPOSITORY' => $this->getRepository()->getCallsign(), self::ENV_USER => $this->getViewer()->getUsername(), self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), ); $directories = $this->getRepository()->getHookDirectories(); foreach ($directories as $directory) { $hooks = $this->getExecutablesInDirectory($directory); sort($hooks); foreach ($hooks as $hook) { // NOTE: We're explicitly running the hooks in sequential order to // make this more predictable. $future = id(new ExecFuture('%s %Ls', $hook, $args)) ->setEnv($env, $wipe_process_env = false) ->write($stdin); list($err, $stdout, $stderr) = $future->resolve(); if (!$err) { // This hook ran OK, but echo its output in case there was something // informative. $console->writeOut('%s', $stdout); $console->writeErr('%s', $stderr); continue; } $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL; $this->rejectDetails = basename($hook); throw new DiffusionCommitHookRejectException( pht( "This push was rejected by custom hook script '%s':\n\n%s%s", basename($hook), $stdout, $stderr)); } } } private function getExecutablesInDirectory($directory) { $executables = array(); if (!Filesystem::pathExists($directory)) { return $executables; } foreach (Filesystem::listDirectory($directory) as $path) { $full_path = $directory.DIRECTORY_SEPARATOR.$path; if (!is_executable($full_path)) { // Don't include non-executable files. continue; } if (basename($full_path) == 'README') { // Don't include README, even if it is marked as executable. It almost // certainly got caught in the crossfire of a sweeping `chmod`, since // users do this with some frequency. continue; } $executables[] = $full_path; } return $executables; } /* -( Mercurial )---------------------------------------------------------- */ private function findMercurialRefUpdates() { $hook = $this->getMercurialHook(); switch ($hook) { case 'pretxnchangegroup': return $this->findMercurialChangegroupRefUpdates(); case 'prepushkey': return $this->findMercurialPushKeyRefUpdates(); default: throw new Exception(pht('Unrecognized hook "%s"!', $hook)); } } private function findMercurialChangegroupRefUpdates() { $hg_node = getenv('HG_NODE'); if (!$hg_node) { throw new Exception( pht( 'Expected %s in environment!', 'HG_NODE')); } // NOTE: We need to make sure this is passed to subprocesses, or they won't // be able to see new commits. Mercurial uses this as a marker to determine // whether the pending changes are visible or not. $_ENV['HG_PENDING'] = getenv('HG_PENDING'); $repository = $this->getRepository(); $futures = array(); foreach (array('old', 'new') as $key) { $futures[$key] = $repository->getLocalCommandFuture( 'heads --template %s', '{node}\1{branch}\2'); } // Wipe HG_PENDING out of the old environment so we see the pre-commit // state of the repository. $futures['old']->updateEnv('HG_PENDING', null); $futures['commits'] = $repository->getLocalCommandFuture( 'log --rev %s --template %s', hgsprintf('%s:%s', $hg_node, 'tip'), '{node}\1{branch}\2'); // Resolve all of the futures now. We don't need the 'commits' future yet, // but it simplifies the logic to just get it out of the way. foreach (new FutureIterator($futures) as $future) { $future->resolve(); } list($commit_raw) = $futures['commits']->resolvex(); $commit_map = $this->parseMercurialCommits($commit_raw); $this->mercurialCommits = $commit_map; // NOTE: `hg heads` exits with an error code and no output if the repository // has no heads. Most commonly this happens on a new repository. We know // we can run `hg` successfully since the `hg log` above didn't error, so // just ignore the error code. list($err, $old_raw) = $futures['old']->resolve(); $old_refs = $this->parseMercurialHeads($old_raw); list($err, $new_raw) = $futures['new']->resolve(); $new_refs = $this->parseMercurialHeads($new_raw); $all_refs = array_keys($old_refs + $new_refs); $ref_updates = array(); foreach ($all_refs as $ref) { $old_heads = idx($old_refs, $ref, array()); $new_heads = idx($new_refs, $ref, array()); sort($old_heads); sort($new_heads); if (!$old_heads && !$new_heads) { // This should never be possible, as it makes no sense. Explode. throw new Exception( pht( 'Mercurial repository has no new or old heads for branch "%s" '. 'after push. This makes no sense; rejecting change.', $ref)); } if ($old_heads === $new_heads) { // No changes to this branch, so skip it. continue; } $stray_heads = array(); if ($old_heads && !$new_heads) { // This is a branch deletion with "--close-branch". $head_map = array(); foreach ($old_heads as $old_head) { $head_map[$old_head] = array(self::EMPTY_HASH); } } else if (count($old_heads) > 1) { // HORRIBLE: In Mercurial, branches can have multiple heads. If the // old branch had multiple heads, we need to figure out which new // heads descend from which old heads, so we can tell whether you're // actively creating new heads (dangerous) or just working in a // repository that's already full of garbage (strongly discouraged but // not as inherently dangerous). These cases should be very uncommon. // NOTE: We're only looking for heads on the same branch. The old // tip of the branch may be the branchpoint for other branches, but that // is OK. $dfutures = array(); foreach ($old_heads as $old_head) { $dfutures[$old_head] = $repository->getLocalCommandFuture( 'log --branch %s --rev %s --template %s', $ref, hgsprintf('(descendants(%s) and head())', $old_head), '{node}\1'); } $head_map = array(); foreach (new FutureIterator($dfutures) as $future_head => $dfuture) { list($stdout) = $dfuture->resolvex(); $descendant_heads = array_filter(explode("\1", $stdout)); if ($descendant_heads) { // This old head has at least one descendant in the push. $head_map[$future_head] = $descendant_heads; } else { // This old head has no descendants, so it is being deleted. $head_map[$future_head] = array(self::EMPTY_HASH); } } // Now, find all the new stray heads this push creates, if any. These // are new heads which do not descend from the old heads. $seen = array_fuse(array_mergev($head_map)); foreach ($new_heads as $new_head) { if ($new_head === self::EMPTY_HASH) { // If a branch head is being deleted, don't insert it as an add. continue; } if (empty($seen[$new_head])) { $head_map[self::EMPTY_HASH][] = $new_head; } } } else if ($old_heads) { $head_map[head($old_heads)] = $new_heads; } else { $head_map[self::EMPTY_HASH] = $new_heads; } foreach ($head_map as $old_head => $child_heads) { foreach ($child_heads as $new_head) { if ($new_head === $old_head) { continue; } $ref_flags = 0; $dangerous = null; if ($old_head == self::EMPTY_HASH) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } $deletes_existing_head = ($new_head == self::EMPTY_HASH); $splits_existing_head = (count($child_heads) > 1); $creates_duplicate_head = ($old_head == self::EMPTY_HASH) && (count($head_map) > 1); if ($splits_existing_head || $creates_duplicate_head) { $readable_child_heads = array(); foreach ($child_heads as $child_head) { $readable_child_heads[] = substr($child_head, 0, 12); } $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS; if ($splits_existing_head) { // We're splitting an existing head into two or more heads. // This is dangerous, and a super bad idea. Note that we're only // raising this if you're actively splitting a branch head. If a // head split in the past, we don't consider appends to it // to be dangerous. $dangerous = pht( "The change you're attempting to push splits the head of ". "branch '%s' into multiple heads: %s. This is inadvisable ". "and dangerous.", $ref, implode(', ', $readable_child_heads)); } else { // We're adding a second (or more) head to a branch. The new // head is not a descendant of any old head. $dangerous = pht( "The change you're attempting to push creates new, divergent ". "heads for the branch '%s': %s. This is inadvisable and ". "dangerous.", $ref, implode(', ', $readable_child_heads)); } } if ($deletes_existing_head) { // TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE // if we are also creating at least one other head to replace // this one. // NOTE: In Git, this is a dangerous change, but it is not dangerous // in Mercurial. Mercurial branches are version controlled, and // Mercurial does not prompt you for any special flags when pushing // a `--close-branch` commit by default. $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } $ref_update = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH) ->setRefName($ref) ->setRefOld($old_head) ->setRefNew($new_head) ->setChangeFlags($ref_flags); if ($dangerous !== null) { $ref_update->attachDangerousChangeDescription($dangerous); } $ref_updates[] = $ref_update; } } } return $ref_updates; } private function findMercurialPushKeyRefUpdates() { $key_namespace = getenv('HG_NAMESPACE'); if ($key_namespace === 'phases') { // Mercurial changes commit phases as part of normal push operations. We // just ignore these, as they don't seem to represent anything // interesting. return array(); } $key_name = getenv('HG_KEY'); $key_old = getenv('HG_OLD'); if (!strlen($key_old)) { $key_old = null; } $key_new = getenv('HG_NEW'); if (!strlen($key_new)) { $key_new = null; } if ($key_namespace !== 'bookmarks') { throw new Exception( pht( "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ". "Rejecting push.", $key_namespace, $key_name, coalesce($key_old, pht('null')), coalesce($key_new, pht('null')))); } if ($key_old === $key_new) { // We get a callback when the bookmark doesn't change. Just ignore this, // as it's a no-op. return array(); } $ref_flags = 0; $merge_base = null; if ($key_old === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; } else if ($key_new === null) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE; } else { list($merge_base_raw) = $this->getRepository()->execxLocalCommand( 'log --template %s --rev %s', '{node}', hgsprintf('ancestor(%s, %s)', $key_old, $key_new)); if (strlen(trim($merge_base_raw))) { $merge_base = trim($merge_base_raw); } if ($merge_base && ($merge_base === $key_old)) { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; } else { $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE; } } $ref_update = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK) ->setRefName($key_name) ->setRefOld(coalesce($key_old, self::EMPTY_HASH)) ->setRefNew(coalesce($key_new, self::EMPTY_HASH)) ->setChangeFlags($ref_flags); return array($ref_update); } private function findMercurialContentUpdates(array $ref_updates) { $content_updates = array(); foreach ($this->mercurialCommits as $commit => $branches) { $content_updates[$commit] = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($commit) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD); } return $content_updates; } private function parseMercurialCommits($raw) { $commits_lines = explode("\2", $raw); $commits_lines = array_filter($commits_lines); $commit_map = array(); foreach ($commits_lines as $commit_line) { list($node, $branch) = explode("\1", $commit_line); $commit_map[$node] = array($branch); } return $commit_map; } private function parseMercurialHeads($raw) { $heads_map = $this->parseMercurialCommits($raw); $heads = array(); foreach ($heads_map as $commit => $branches) { foreach ($branches as $branch) { $heads[$branch][] = $commit; } } return $heads; } /* -( Subversion )--------------------------------------------------------- */ private function findSubversionRefUpdates() { // Subversion doesn't have any kind of mutable ref metadata. return array(); } private function findSubversionContentUpdates(array $ref_updates) { list($youngest) = execx( 'svnlook youngest %s', $this->subversionRepository); $ref_new = (int)$youngest + 1; $ref_flags = 0; $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD; $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND; $ref_content = $this->newPushLog() ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT) ->setRefNew($ref_new) ->setChangeFlags($ref_flags); return array($ref_content); } /* -( Internals )---------------------------------------------------------- */ private function newPushLog() { // NOTE: We generate PHIDs up front so the Herald transcripts can pick them // up. $phid = id(new PhabricatorRepositoryPushLog())->generatePHID(); return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) ->setPHID($phid) ->setRepositoryPHID($this->getRepository()->getPHID()) ->attachRepository($this->getRepository()) ->setEpoch(time()); } private function newPushEvent() { $viewer = $this->getViewer(); return PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) ->setRepositoryPHID($this->getRepository()->getPHID()) ->setRemoteAddress($this->getRemoteAddressForLog()) ->setRemoteProtocol($this->getRemoteProtocol()) ->setEpoch(time()); } public function loadChangesetsForCommit($identifier) { $byte_limit = HeraldCommitAdapter::getEnormousByteLimit(); $time_limit = HeraldCommitAdapter::getEnormousTimeLimit(); $vcs = $this->getRepository()->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // For git and hg, we can use normal commands. $drequest = DiffusionRequest::newFromDictionary( array( 'repository' => $this->getRepository(), 'user' => $this->getViewer(), 'commit' => $identifier, )); $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) ->setTimeout($time_limit) ->setByteLimit($byte_limit) ->setLinesOfContext(0) ->loadRawDiff(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // TODO: This diff has 3 lines of context, which produces slightly // incorrect "added file content" and "removed file content" results. // This may also choke on binaries, but "svnlook diff" does not support // the "--diff-cmd" flag. // For subversion, we need to use `svnlook`. $future = new ExecFuture( 'svnlook diff -t %s %s', $this->subversionTransaction, $this->subversionRepository); $future->setTimeout($time_limit); $future->setStdoutSizeLimit($byte_limit); $future->setStderrSizeLimit($byte_limit); list($raw_diff) = $future->resolvex(); break; default: throw new Exception(pht("Unknown VCS '%s!'", $vcs)); } if (strlen($raw_diff) >= $byte_limit) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %d '. 'bytes). Herald can not process it.', $byte_limit)); } if (!strlen($raw_diff)) { // If the commit is actually empty, just return no changesets. return array(); } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw_diff); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes); return $diff->getChangesets(); } public function loadCommitRefForCommit($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: return id(new DiffusionLowLevelCommitQuery()) ->setRepository($repository) ->withIdentifier($identifier) ->execute(); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // For subversion, we need to use `svnlook`. list($message) = execx( 'svnlook log -t %s %s', $this->subversionTransaction, $this->subversionRepository); return id(new DiffusionCommitRef()) ->setMessage($message); break; default: throw new Exception(pht("Unknown VCS '%s!'", $vcs)); } } public function loadBranches($identifier) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: return idx($this->gitCommits, $identifier, array()); case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: // NOTE: This will be "the branch the commit was made to", not // "a list of all branch heads which descend from the commit". // This is consistent with Mercurial, but possibly confusing. return idx($this->mercurialCommits, $identifier, array()); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // Subversion doesn't have branches. return array(); } } private function loadCommitInfoForWorker(array $all_updates) { $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; $map = array(); foreach ($all_updates as $update) { if ($update->getRefType() != $type_commit) { continue; } $map[$update->getRefNew()] = array(); } foreach ($map as $identifier => $info) { $ref = $this->loadCommitRefForCommit($identifier); $map[$identifier] += array( 'summary' => $ref->getSummary(), 'branches' => $this->loadBranches($identifier), ); } return $map; } private function isInitialImport(array $all_updates) { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // There is no meaningful way to import history into Subversion by // pushing. return false; default: break; } // Now, apply a heuristic to guess whether this is a normal commit or // an initial import. We guess something is an initial import if: // // - the repository is currently empty; and // - it pushes more than 7 commits at once. // // The number "7" is chosen arbitrarily as seeming reasonable. We could // also look at author data (do the commits come from multiple different // authors?) and commit date data (is the oldest commit more than 48 hours // old), but we don't have immediate access to those and this simple // heruistic might be good enough. $commit_count = 0; $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; foreach ($all_updates as $update) { if ($update->getRefType() != $type_commit) { continue; } $commit_count++; } if ($commit_count <= 7) { // If this pushes a very small number of commits, assume it's an // initial commit or stack of a few initial commits. return false; } $any_commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withRepository($repository) ->setLimit(1) ->execute(); if ($any_commits) { // If the repository already has commits, this isn't an import. return false; } return true; } } diff --git a/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php new file mode 100644 index 0000000000..36b01b96f8 --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php @@ -0,0 +1,33 @@ +getRule(); + return $this->applyAuditors($effect->getTarget(), $rule); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new DiffusionAuditorDatasource(); + } + + public function renderActionDescription($value) { + return pht('Add auditors: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php new file mode 100644 index 0000000000..d27876d40e --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php @@ -0,0 +1,30 @@ +getRule(); + $phid = $rule->getAuthorPHID(); + return $this->applyAuditors(array($phid), $rule); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Add rule author as auditor.'); + } + +} diff --git a/src/applications/diffusion/herald/DiffusionAuditorsHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsHeraldAction.php new file mode 100644 index 0000000000..32830cb673 --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionAuditorsHeraldAction.php @@ -0,0 +1,76 @@ +getAdapter(); + $object = $adapter->getObject(); + + $auditors = $object->getAudits(); + $auditors = mpull($auditors, null, 'getAuditorPHID'); + $current = array_keys($auditors); + + $allowed_types = array( + PhabricatorPeopleUserPHIDType::TYPECONST, + PhabricatorProjectProjectPHIDType::TYPECONST, + PhabricatorOwnersPackagePHIDType::TYPECONST, + ); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + // TODO: Convert this to be translatable, structured data eventually. + $reason_map = array(); + foreach ($phids as $phid) { + $reason_map[$phid][] = pht('%s Triggered Audit', $rule->getMonogram()); + } + + $xaction = $adapter->newTransaction() + ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) + ->setNewValue($phids) + ->setMetadataValue( + 'auditStatus', + PhabricatorAuditStatusConstants::AUDIT_REQUIRED) + ->setMetadataValue('auditReasonMap', $reason_map); + + $adapter->queueTransaction($xaction); + + $this->logEffect(self::DO_ADD_AUDITORS, $phids); + } + + protected function getActionEffectMap() { + return array( + self::DO_ADD_AUDITORS => array( + 'icon' => 'fa-user', + 'color' => 'green', + 'name' => pht('Added Auditors'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_ADD_AUDITORS: + return pht( + 'Added %s auditor(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + } + } + +} diff --git a/src/applications/diffusion/herald/DiffusionBlockHeraldAction.php b/src/applications/diffusion/herald/DiffusionBlockHeraldAction.php new file mode 100644 index 0000000000..32656d78fe --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionBlockHeraldAction.php @@ -0,0 +1,56 @@ +logEffect(self::DO_BLOCK); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_TEXT; + } + + public function renderActionDescription($value) { + return pht('Block push with message: %s', $value); + } + + protected function getActionEffectMap() { + return array( + self::DO_BLOCK => array( + 'icon' => 'fa-stop', + 'color' => 'red', + 'name' => pht('Blocked Push'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_BLOCK: + return pht('Blocked push.'); + } + } +} diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index 0210185ed9..ab3e850475 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -1,372 +1,321 @@ commit = $this->newObject(); } public function getObject() { return $this->commit; } public function getAdapterContentType() { return 'commit'; } public function getAdapterContentName() { return pht('Commits'); } public function getAdapterContentDescription() { return pht( "React to new commits appearing in tracked repositories.\n". "Commit rules can send email, flag commits, trigger audits, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; default: return false; } } public function canTriggerOnObject($object) { if ($object instanceof PhabricatorRepository) { return true; } if ($object instanceof PhabricatorProject) { return true; } return false; } public function getTriggerObjectPHIDs() { return array_merge( array( $this->repository->getPHID(), $this->getPHID(), ), $this->repository->getProjectPHIDs()); } public function explainValidTriggerObjects() { return pht('This rule can trigger for **repositories** and **projects**.'); } - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_AUDIT, - self::ACTION_APPLY_BUILD_PLANS, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_FLAG, - self::ACTION_AUDIT, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } - } - public static function newLegacyAdapter( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $commit_data) { $object = new HeraldCommitAdapter(); $commit->attachRepository($repository); $object->repository = $repository; $object->commit = $commit; $object->commitData = $commit_data; return $object; } public function setCommit(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIDs(array($commit->getRepositoryID())) ->needProjectPHIDs(true) ->executeOne(); if (!$repository) { throw new Exception(pht('Unable to load repository!')); } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { throw new Exception(pht('Unable to load commit data!')); } $this->commit = clone $commit; $this->commit->attachRepository($repository); $this->commit->attachCommitData($data); $this->repository = $repository; $this->commitData = $data; return $this; } - public function getAuditMap() { - return $this->auditMap; - } - - public function getBuildPlans() { - return $this->buildPlans; - } - public function getHeraldName() { return 'r'. $this->repository->getCallsign(). $this->commit->getCommitIdentifier(); } public function loadAffectedPaths() { if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->repository, $this->commit, PhabricatorUser::getOmnipotentUser()); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackages() { if ($this->auditNeededPackages === null) { $status_arr = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::CONCERNED, ); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s AND auditStatus IN (%Ls)', $this->commit->getPHID(), $status_arr); $packages = mpull($requests, 'getAuditorPHID'); $this->auditNeededPackages = $packages; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { if ($this->affectedRevision === null) { $this->affectedRevision = false; $data = $this->commitData; $revision_id = $data->getCommitDetail('differential.revisionID'); if ($revision_id) { // NOTE: The Herald rule owner might not actually have access to // the revision, and can control which revision a commit is // associated with by putting text in the commit message. However, // the rules they can write against revisions don't actually expose // anything interesting, so it seems reasonable to load unconditionally // here. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if ($revision) { $this->affectedRevision = $revision; } } } return $this->affectedRevision; } public static function getEnormousByteLimit() { return 1024 * 1024 * 1024; // 1GB } public static function getEnormousTimeLimit() { return 60 * 15; // 15 Minutes } private function loadCommitDiff() { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), )); $byte_limit = self::getEnormousByteLimit(); $raw = DiffusionQuery::callConduitWithDiffusionRequest( PhabricatorUser::getOmnipotentUser(), $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => self::getEnormousTimeLimit(), 'byteLimit' => $byte_limit, 'linesOfContext' => 0, )); if (strlen($raw) >= $byte_limit) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %d bytes). '. 'Herald can not process it.', $byte_limit)); } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes); return $diff; } public function isDiffEnormous() { $this->loadDiffContent('*'); return ($this->commitDiff instanceof Exception); } public function loadDiffContent($type) { if ($this->commitDiff === null) { try { $this->commitDiff = $this->loadCommitDiff(); } catch (Exception $ex) { $this->commitDiff = $ex; phlog($ex); } } if ($this->commitDiff instanceof Exception) { $ex = $this->commitDiff; $ex_class = get_class($ex); $ex_message = pht('Failed to load changes: %s', $ex->getMessage()); return array( '<'.$ex_class.'>' => $ex_message, ); } $changes = $this->commitDiff->getChangesets(); $result = array(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { switch ($type) { case '-': $lines[] = $hunk->makeOldFile(); break; case '+': $lines[] = $hunk->makeNewFile(); break; case '*': $lines[] = $hunk->makeChanges(); break; default: throw new Exception(pht("Unknown content selection '%s'!", $type)); } } $result[$change->getFilename()] = implode("\n", $lines); } return $result; } - public function applyHeraldEffects(array $effects) { - assert_instances_of($effects, 'HeraldEffect'); - $result = array(); - foreach ($effects as $effect) { - $action = $effect->getAction(); - switch ($action) { - case self::ACTION_AUDIT: - foreach ($effect->getTarget() as $phid) { - if (empty($this->auditMap[$phid])) { - $this->auditMap[$phid] = array(); - } - $this->auditMap[$phid][] = $effect->getRule()->getID(); - } - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Triggered an audit.')); - break; - case self::ACTION_APPLY_BUILD_PLANS: - foreach ($effect->getTarget() as $phid) { - $this->buildPlans[] = $phid; - } - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Applied build plans.')); - break; - default: - $result[] = $this->applyStandardEffect($effect); - break; - } - } - return $result; +/* -( HarbormasterBuildableAdapterInterface )------------------------------ */ + + + public function getHarbormasterBuildablePHID() { + return $this->getObject()->getPHID(); + } + + public function getHarbormasterContainerPHID() { + return $this->getObject()->getRepository()->getPHID(); + } + + public function getQueuedHarbormasterBuildPlanPHIDs() { + return $this->buildPlanPHIDs; + } + + public function queueHarbormasterBuildPlanPHID($phid) { + $this->buildPlanPHIDs[] = $phid; } } diff --git a/src/applications/diffusion/herald/HeraldPreCommitAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitAdapter.php index 07f1f0d2c7..6b5b94b578 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitAdapter.php @@ -1,119 +1,76 @@ log = $log; return $this; } public function setHookEngine(DiffusionCommitHookEngine $engine) { $this->hookEngine = $engine; return $this; } public function getHookEngine() { return $this->hookEngine; } public function getAdapterApplicationClass() { return 'PhabricatorDiffusionApplication'; } protected function initializeNewAdapter() { $this->log = new PhabricatorRepositoryPushLog(); } public function isSingleEventAdapter() { return true; } public function getObject() { return $this->log; } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return true; default: return false; } } public function canTriggerOnObject($object) { if ($object instanceof PhabricatorRepository) { return true; } if ($object instanceof PhabricatorProject) { return true; } return false; } public function explainValidTriggerObjects() { return pht('This rule can trigger for **repositories** or **projects**.'); } public function getTriggerObjectPHIDs() { return array_merge( array( $this->hookEngine->getRepository()->getPHID(), $this->getPHID(), ), $this->hookEngine->getRepository()->getProjectPHIDs()); } - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: - return array_merge( - array( - self::ACTION_BLOCK, - self::ACTION_EMAIL, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return array_merge( - array( - self::ACTION_EMAIL, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } - } - - public function applyHeraldEffects(array $effects) { - assert_instances_of($effects, 'HeraldEffect'); - - $result = array(); - foreach ($effects as $effect) { - $action = $effect->getAction(); - switch ($action) { - case self::ACTION_BLOCK: - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Blocked push.')); - break; - default: - $result[] = $this->applyStandardEffect($effect); - break; - } - } - - return $result; - } - } diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index 7098e7416f..45f8b063af 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,615 +1,615 @@ 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) { $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 underyling 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; } /** * 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 = $repository_ids; return $this; } public function needCommitData($need) { $this->needCommitData = $need; return $this; } public function needAuditRequests($need) { $this->needAuditRequests = $need; return $this; } /** * Returns true if we should join the audit table, either because we're * interested in the information if it's available or because matching rows * must always have it. */ private function shouldJoinAudits() { return $this->auditStatus || $this->rowsMustHaveAudits(); } /** * Return true if we should `JOIN` (vs `LEFT JOIN`) the audit table, because * matching commits will always have audit rows. */ private function rowsMustHaveAudits() { return $this->auditIDs || $this->auditorPHIDs || $this->auditAwaitingUser; } public function withAuditIDs(array $ids) { $this->auditIDs = $ids; return $this; } public function withAuditorPHIDs(array $auditor_phids) { $this->auditorPHIDs = $auditor_phids; return $this; } public function withAuditAwaitingUser(PhabricatorUser $user) { $this->auditAwaitingUser = $user; return $this; } public function withAuditStatus($status) { $this->auditStatus = $status; 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 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(); } } protected function loadPage() { $table = new PhabricatorRepositoryCommit(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT commit.* FROM %T commit %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)); return $table->loadAllFromArray($data); } 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 = array_fuse($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) { 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); } } // TODO: This should just be `needAuditRequests`, not `shouldJoinAudits()`, // but leave that for a future diff. if ($this->needAuditRequests || $this->shouldJoinAudits()) { $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); } } } return $commits; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->repositoryPHIDs !== null) { - $map_repositories = id (new PhabricatorRepositoryQuery()) + $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->ids !== null) { $where[] = qsprintf( $conn_r, 'commit.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'commit.phid IN (%Ls)', $this->phids); } if ($this->repositoryIDs !== null) { $where[] = qsprintf( $conn_r, 'commit.repositoryID IN (%Ld)', $this->repositoryIDs); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'commit.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->epochMin !== null) { $where[] = qsprintf( $conn_r, 'commit.epoch >= %d', $this->epochMin); } if ($this->epochMax !== null) { $where[] = qsprintf( $conn_r, 'commit.epoch <= %d', $this->epochMax); } if ($this->importing !== null) { if ($this->importing) { $where[] = qsprintf( $conn_r, '(commit.importStatus & %d) != %d', PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); } else { $where[] = qsprintf( $conn_r, '(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->getCallsign(); } } if ($repo === null) { if (strlen($commit_identifier) < $min_unqualified) { continue; } $bare[] = $commit_identifier; } else { $refs[] = array( 'callsign' => $repo, 'identifier' => $commit_identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn_r, '(commit.commitIdentifier LIKE %> AND '. 'LENGTH(commit.commitIdentifier) = 40)', $identifier); } if ($refs) { $callsigns = ipull($refs, 'callsign'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIdentifiers($callsigns); $repos->execute(); $repos = $repos->getIdentifierMap(); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['callsign']); if (!$repo) { continue; } if ($repo->isSVN()) { if (!ctype_digit($ref['identifier'])) { continue; } $sql[] = qsprintf( $conn_r, '(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; } $sql[] = qsprintf( $conn_r, '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)', $repo->getID(), $ref['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[] = '('.implode(' OR ', $sql).')'; } if ($this->auditIDs !== null) { $where[] = qsprintf( $conn_r, 'audit.id IN (%Ld)', $this->auditIDs); } if ($this->auditorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'audit.auditorPHID IN (%Ls)', $this->auditorPHIDs); } if ($this->auditAwaitingUser) { $awaiting_user_phid = $this->auditAwaitingUser->getPHID(); // Exclude package and project audits associated with commits where // the user is the author. $where[] = qsprintf( $conn_r, '(commit.authorPHID IS NULL OR commit.authorPHID != %s) OR (audit.auditorPHID = %s)', $awaiting_user_phid, $awaiting_user_phid); } $status = $this->auditStatus; if ($status !== null) { switch ($status) { case self::AUDIT_STATUS_PARTIAL: $where[] = qsprintf( $conn_r, 'commit.auditStatus = %d', PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED); break; case self::AUDIT_STATUS_ACCEPTED: $where[] = qsprintf( $conn_r, 'commit.auditStatus = %d', PhabricatorAuditCommitStatusConstants::FULLY_AUDITED); break; case self::AUDIT_STATUS_CONCERN: $where[] = qsprintf( $conn_r, 'audit.auditStatus = %s', PhabricatorAuditStatusConstants::CONCERNED); break; case self::AUDIT_STATUS_OPEN: $where[] = qsprintf( $conn_r, 'audit.auditStatus in (%Ls)', PhabricatorAuditStatusConstants::getOpenStatusConstants()); if ($this->auditAwaitingUser) { $where[] = qsprintf( $conn_r, 'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s', PhabricatorAuditStatusConstants::RESIGNED); } break; case self::AUDIT_STATUS_ANY: break; default: $valid = array( self::AUDIT_STATUS_ANY, self::AUDIT_STATUS_OPEN, self::AUDIT_STATUS_CONCERN, self::AUDIT_STATUS_ACCEPTED, self::AUDIT_STATUS_PARTIAL, ); throw new Exception( pht( "Unknown audit status '%s'! Valid statuses are: %s.", $status, implode(', ', $valid))); } } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } protected function didFilterResults(array $filtered) { if ($this->identifierMap) { foreach ($this->identifierMap as $name => $commit) { if (isset($filtered[$commit->getPHID()])) { unset($this->identifierMap[$name]); } } } } protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); $audit_request = new PhabricatorRepositoryAuditRequest(); if ($this->shouldJoinAudits()) { $joins[] = qsprintf( $conn_r, '%Q %T audit ON commit.phid = audit.commitPHID', ($this->rowsMustHaveAudits() ? 'JOIN' : 'LEFT JOIN'), $audit_request->getTableName()); } if ($this->auditAwaitingUser) { // Join the request table on the awaiting user's requests, so we can // filter out package and project requests which the user has resigned // from. $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T awaiting ON audit.commitPHID = awaiting.commitPHID AND awaiting.auditorPHID = %s', $audit_request->getTableName(), $this->auditAwaitingUser->getPHID()); } if ($joins) { return implode(' ', $joins); } else { return ''; } } protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { $should_group = $this->shouldJoinAudits(); // TODO: Currently, the audit table is missing a unique key, so we may // require a GROUP BY if we perform this join. See T1768. This can be // removed once the table has the key. if ($this->auditAwaitingUser) { $should_group = true; } if ($should_group) { return 'GROUP BY commit.id'; } else { return ''; } } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } } diff --git a/src/applications/drydock/controller/DrydockBlueprintCreateController.php b/src/applications/drydock/controller/DrydockBlueprintCreateController.php index 56e0228077..5398a2327d 100644 --- a/src/applications/drydock/controller/DrydockBlueprintCreateController.php +++ b/src/applications/drydock/controller/DrydockBlueprintCreateController.php @@ -1,80 +1,79 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $this->requireApplicationCapability( DrydockCreateBlueprintsCapability::CAPABILITY); $implementations = DrydockBlueprintImplementation::getAllBlueprintImplementations(); $errors = array(); $e_blueprint = null; if ($request->isFormPost()) { $class = $request->getStr('blueprint-type'); if (!isset($implementations[$class])) { $e_blueprint = pht('Required'); $errors[] = pht('You must choose a blueprint type.'); } if (!$errors) { $edit_uri = $this->getApplicationURI('blueprint/edit/?class='.$class); return id(new AphrontRedirectResponse())->setURI($edit_uri); } } $control = id(new AphrontFormRadioButtonControl()) ->setName('blueprint-type') ->setLabel(pht('Blueprint Type')) ->setError($e_blueprint); foreach ($implementations as $implementation_name => $implementation) { $disabled = !$implementation->isEnabled(); $control->addButton( $implementation_name, $implementation->getBlueprintName(), array( pht('Provides: %s', $implementation->getType()), phutil_tag('br'), phutil_tag('br'), $implementation->getDescription(), ), $disabled ? 'disabled' : null, $disabled); } $title = pht('Create New Blueprint'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('New Blueprint')); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($control) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI('blueprint/')) ->setValue(pht('Continue'))); $box = id(new PHUIObjectBoxView()) ->setFormErrors($errors) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/drydock/controller/DrydockBlueprintEditController.php b/src/applications/drydock/controller/DrydockBlueprintEditController.php index 4211c80b4c..1c75bf708d 100644 --- a/src/applications/drydock/controller/DrydockBlueprintEditController.php +++ b/src/applications/drydock/controller/DrydockBlueprintEditController.php @@ -1,174 +1,168 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { + if ($id) { $blueprint = id(new DrydockBlueprintQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$blueprint) { return new Aphront404Response(); } $impl = $blueprint->getImplementation(); - $cancel_uri = $this->getApplicationURI('blueprint/'.$this->id.'/'); + $cancel_uri = $this->getApplicationURI('blueprint/'.$id.'/'); } else { $this->requireApplicationCapability( DrydockCreateBlueprintsCapability::CAPABILITY); $class = $request->getStr('class'); $impl = DrydockBlueprintImplementation::getNamedImplementation($class); if (!$impl || !$impl->isEnabled()) { return new Aphront400Response(); } $blueprint = DrydockBlueprint::initializeNewBlueprint($viewer); $blueprint->setClassName($class); $cancel_uri = $this->getApplicationURI('blueprint/'); } $field_list = PhabricatorCustomField::getObjectFields( $blueprint, PhabricatorCustomField::ROLE_EDIT); $field_list ->setViewer($viewer) ->readFieldsFromStorage($blueprint); $v_name = $blueprint->getBlueprintName(); $e_name = true; $errors = array(); $validation_exception = null; if ($request->isFormPost()) { $v_view_policy = $request->getStr('viewPolicy'); $v_edit_policy = $request->getStr('editPolicy'); $v_name = $request->getStr('name'); if (!strlen($v_name)) { $e_name = pht('Required'); $errors[] = pht('You must name this blueprint.'); } if (!$errors) { $xactions = array(); $xactions = $field_list->buildFieldTransactionsFromRequest( new DrydockBlueprintTransaction(), $request); $xactions[] = id(new DrydockBlueprintTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view_policy); $xactions[] = id(new DrydockBlueprintTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($v_edit_policy); $xactions[] = id(new DrydockBlueprintTransaction()) ->setTransactionType(DrydockBlueprintTransaction::TYPE_NAME) ->setNewValue($v_name); $editor = id(new DrydockBlueprintEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($blueprint, $xactions); $id = $blueprint->getID(); $save_uri = $this->getApplicationURI("blueprint/{$id}/"); return id(new AphrontRedirectResponse())->setURI($save_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($blueprint) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('class', $request->getStr('class')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Blueprint Type')) ->setValue($impl->getBlueprintName())) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($blueprint) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($blueprint) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)); $field_list->appendFieldsToForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($blueprint->getID()) { $title = pht('Edit Blueprint'); $header = pht('Edit Blueprint %d', $blueprint->getID()); $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); $crumbs->addTextCrumb(pht('Edit')); $submit = pht('Save Blueprint'); } else { $title = pht('New Blueprint'); $header = pht('New Blueprint'); $crumbs->addTextCrumb(pht('New Blueprint')); $submit = pht('Create Blueprint'); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue($submit) ->addCancelButton($cancel_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setValidationException($validation_exception) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/drydock/controller/DrydockBlueprintListController.php b/src/applications/drydock/controller/DrydockBlueprintListController.php index 31e716691d..ff09f19ff8 100644 --- a/src/applications/drydock/controller/DrydockBlueprintListController.php +++ b/src/applications/drydock/controller/DrydockBlueprintListController.php @@ -1,40 +1,36 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $request = $this->getRequest(); $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new DrydockBlueprintSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } protected function buildApplicationCrumbs() { $can_create = $this->hasApplicationCapability( DrydockCreateBlueprintsCapability::CAPABILITY); $crumbs = parent::buildApplicationCrumbs(); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('New Blueprint')) ->setHref($this->getApplicationURI('/blueprint/create/')) ->setDisabled(!$can_create) ->setWorkflow(!$can_create) ->setIcon('fa-plus-square')); return $crumbs; } } diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php index be5229972e..33a27264b8 100644 --- a/src/applications/drydock/controller/DrydockBlueprintViewController.php +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -1,129 +1,123 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $blueprint = id(new DrydockBlueprintQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$blueprint) { return new Aphront404Response(); } $title = $blueprint->getBlueprintName(); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($blueprint); $actions = $this->buildActionListView($blueprint); $properties = $this->buildPropertyListView($blueprint, $actions); $blueprint_uri = 'blueprint/'.$blueprint->getID().'/'; $blueprint_uri = $this->getApplicationURI($blueprint_uri); $resources = id(new DrydockResourceQuery()) ->withBlueprintPHIDs(array($blueprint->getPHID())) ->setViewer($viewer) ->execute(); $resource_list = id(new DrydockResourceListView()) ->setUser($viewer) ->setResources($resources) ->render(); $resource_list->setNoDataString(pht('This blueprint has no resources.')); $pager = new PHUIPagerView(); $pager->setURI(new PhutilURI($blueprint_uri), 'offset'); $pager->setOffset($request->getInt('offset')); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $field_list = PhabricatorCustomField::getObjectFields( $blueprint, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($viewer) ->readFieldsFromStorage($blueprint); $field_list->appendFieldsToPropertyList( $blueprint, $viewer, $properties); $timeline = $this->buildTransactionTimeline( $blueprint, new DrydockBlueprintTransactionQuery()); $timeline->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $object_box, $resource_list, $timeline, ), array( 'title' => $title, )); } private function buildActionListView(DrydockBlueprint $blueprint) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($blueprint); $uri = '/blueprint/edit/'.$blueprint->getID().'/'; $uri = $this->getApplicationURI($uri); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $blueprint, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) ->setName(pht('Edit Blueprint')) ->setIcon('fa-pencil') ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); return $view; } private function buildPropertyListView( DrydockBlueprint $blueprint, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); $view->addProperty( pht('Type'), $blueprint->getImplementation()->getBlueprintName()); return $view; } } diff --git a/src/applications/drydock/controller/DrydockConsoleController.php b/src/applications/drydock/controller/DrydockConsoleController.php index fd2dff8ba5..118d49ea59 100644 --- a/src/applications/drydock/controller/DrydockConsoleController.php +++ b/src/applications/drydock/controller/DrydockConsoleController.php @@ -1,78 +1,77 @@ setBaseURI(new PhutilURI($this->getApplicationURI())); // These are only used on mobile. $nav->addFilter('blueprint', pht('Blueprints')); $nav->addFilter('resource', pht('Resources')); $nav->addFilter('lease', pht('Leases')); $nav->addFilter('log', pht('Logs')); $nav->selectFilter(null); return $nav; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $menu = id(new PHUIObjectItemListView()) ->setUser($viewer); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Blueprints')) ->setHref($this->getApplicationURI('blueprint/')) ->addAttribute( pht( 'Configure blueprints so Drydock can build resources, like '. 'hosts and working copies.'))); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Resources')) ->setHref($this->getApplicationURI('resource/')) ->addAttribute( pht('View and manage resources Drydock has built, like hosts.'))); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Leases')) ->setHref($this->getApplicationURI('lease/')) ->addAttribute(pht('Manage leases on resources.'))); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Logs')) ->setHref($this->getApplicationURI('log/')) ->addAttribute(pht('View logs.'))); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Console')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Drydock Console')) ->setObjectList($menu); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => pht('Drydock Console'), )); } } diff --git a/src/applications/drydock/controller/DrydockLeaseListController.php b/src/applications/drydock/controller/DrydockLeaseListController.php index f5c0897717..e370467a5f 100644 --- a/src/applications/drydock/controller/DrydockLeaseListController.php +++ b/src/applications/drydock/controller/DrydockLeaseListController.php @@ -1,24 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new DrydockLeaseSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/drydock/controller/DrydockLeaseReleaseController.php b/src/applications/drydock/controller/DrydockLeaseReleaseController.php index 1779edf1b2..4bac0330ba 100644 --- a/src/applications/drydock/controller/DrydockLeaseReleaseController.php +++ b/src/applications/drydock/controller/DrydockLeaseReleaseController.php @@ -1,65 +1,59 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $lease = id(new DrydockLeaseQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$lease) { return new Aphront404Response(); } $lease_uri = '/lease/'.$lease->getID().'/'; $lease_uri = $this->getApplicationURI($lease_uri); if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Lease Not Active')) ->appendChild( phutil_tag( 'p', array(), pht('You can only release "active" leases.'))) ->addCancelButton($lease_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } if (!$request->isDialogFormPost()) { $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Really release lease?')) ->appendChild( phutil_tag( 'p', array(), pht( 'Releasing a lease may cause trouble for the lease holder and '. 'trigger cleanup of the underlying resource. It can not be '. 'undone. Continue?'))) ->addSubmitButton(pht('Release Lease')) ->addCancelButton($lease_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } $resource = $lease->getResource(); $blueprint = $resource->getBlueprint(); $blueprint->releaseLease($resource, $lease); return id(new AphrontReloadResponse())->setURI($lease_uri); } } diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index d597209b5a..92d215bcbb 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -1,139 +1,133 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$lease) { return new Aphront404Response(); } $lease_uri = $this->getApplicationURI('lease/'.$lease->getID().'/'); $title = pht('Lease %d', $lease->getID()); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = $this->buildActionListView($lease); $properties = $this->buildPropertyListView($lease, $actions); $pager = new PHUIPagerView(); $pager->setURI(new PhutilURI($lease_uri), 'offset'); $pager->setOffset($request->getInt('offset')); $logs = id(new DrydockLogQuery()) ->setViewer($viewer) ->withLeaseIDs(array($lease->getID())) ->executeWithOffsetPager($pager); $log_table = id(new DrydockLogListView()) ->setUser($viewer) ->setLogs($logs) ->render(); $log_table->appendChild($pager); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, $lease_uri); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $log_table, ), array( 'title' => $title, )); } private function buildActionListView(DrydockLease $lease) { $view = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($lease); $id = $lease->getID(); $can_release = ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Release Lease')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("/lease/{$id}/release/")) ->setWorkflow(true) ->setDisabled(!$can_release)); return $view; } private function buildPropertyListView( DrydockLease $lease, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_ACTIVE: $status = pht('Active'); break; case DrydockLeaseStatus::STATUS_RELEASED: $status = pht('Released'); break; case DrydockLeaseStatus::STATUS_EXPIRED: $status = pht('Expired'); break; case DrydockLeaseStatus::STATUS_PENDING: $status = pht('Pending'); break; case DrydockLeaseStatus::STATUS_BROKEN: $status = pht('Broken'); break; default: $status = pht('Unknown'); break; } $view->addProperty( pht('Status'), $status); $view->addProperty( pht('Resource Type'), $lease->getResourceType()); $view->addProperty( pht('Resource'), $lease->getResourceID()); $attributes = $lease->getAttributes(); if ($attributes) { $view->addSectionHeader(pht('Attributes')); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } } return $view; } } diff --git a/src/applications/drydock/controller/DrydockLogListController.php b/src/applications/drydock/controller/DrydockLogListController.php index eaeb411d28..aecf77dc77 100644 --- a/src/applications/drydock/controller/DrydockLogListController.php +++ b/src/applications/drydock/controller/DrydockLogListController.php @@ -1,24 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new DrydockLogSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/drydock/controller/DrydockResourceCloseController.php b/src/applications/drydock/controller/DrydockResourceCloseController.php index 7963612ee4..915bf89452 100644 --- a/src/applications/drydock/controller/DrydockResourceCloseController.php +++ b/src/applications/drydock/controller/DrydockResourceCloseController.php @@ -1,55 +1,49 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $resource = id(new DrydockResourceQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$resource) { return new Aphront404Response(); } $resource_uri = '/resource/'.$resource->getID().'/'; $resource_uri = $this->getApplicationURI($resource_uri); if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Resource Not Open')) ->appendChild(phutil_tag('p', array(), pht( 'You can only close "open" resources.'))) ->addCancelButton($resource_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } if ($request->isFormPost()) { $resource->closeResource(); return id(new AphrontReloadResponse())->setURI($resource_uri); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Really close resource?')) ->appendChild( pht( 'Closing a resource releases all leases and destroys the '. 'resource. It can not be undone. Continue?')) ->addSubmitButton(pht('Close Resource')) ->addCancelButton($resource_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/drydock/controller/DrydockResourceListController.php b/src/applications/drydock/controller/DrydockResourceListController.php index 53ed62a743..d2f34ec25b 100644 --- a/src/applications/drydock/controller/DrydockResourceListController.php +++ b/src/applications/drydock/controller/DrydockResourceListController.php @@ -1,24 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new DrydockResourceSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index 9be8188191..4cc7349dac 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -1,135 +1,129 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $resource = id(new DrydockResourceQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$resource) { return new Aphront404Response(); } $title = pht('Resource %s %s', $resource->getID(), $resource->getName()); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = $this->buildActionListView($resource); $properties = $this->buildPropertyListView($resource, $actions); $resource_uri = 'resource/'.$resource->getID().'/'; $resource_uri = $this->getApplicationURI($resource_uri); $leases = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withResourceIDs(array($resource->getID())) ->execute(); $lease_list = id(new DrydockLeaseListView()) ->setUser($viewer) ->setLeases($leases) ->render(); $lease_list->setNoDataString(pht('This resource has no leases.')); $pager = new PHUIPagerView(); $pager->setURI(new PhutilURI($resource_uri), 'offset'); $pager->setOffset($request->getInt('offset')); $logs = id(new DrydockLogQuery()) ->setViewer($viewer) ->withResourceIDs(array($resource->getID())) ->executeWithOffsetPager($pager); $log_table = id(new DrydockLogListView()) ->setUser($viewer) ->setLogs($logs) ->render(); $log_table->appendChild($pager); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Resource %d', $resource->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $lease_list, $log_table, ), array( 'title' => $title, )); } private function buildActionListView(DrydockResource $resource) { $view = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($resource); $can_close = ($resource->getStatus() == DrydockResourceStatus::STATUS_OPEN); $uri = '/resource/'.$resource->getID().'/close/'; $uri = $this->getApplicationURI($uri); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) ->setName(pht('Close Resource')) ->setIcon('fa-times') ->setWorkflow(true) ->setDisabled(!$can_close)); return $view; } private function buildPropertyListView( DrydockResource $resource, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); $status = $resource->getStatus(); $status = DrydockResourceStatus::getNameForStatus($status); $view->addProperty( pht('Status'), $status); $view->addProperty( pht('Resource Type'), $resource->getType()); // TODO: Load handle. $view->addProperty( pht('Blueprint'), $resource->getBlueprintPHID()); $attributes = $resource->getAttributes(); if ($attributes) { $view->addSectionHeader(pht('Attributes')); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } } return $view; } } diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php index 92718f47f8..d6f3bc0c43 100644 --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -1,96 +1,95 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $table = new PhabricatorFactRaw(); $conn_r = $table->establishConnection('r'); $table_name = $table->getTableName(); $series = $request->getStr('y1'); $specs = PhabricatorFactSpec::newSpecsForFactTypes( PhabricatorFactEngine::loadAllEngines(), array($series)); $spec = idx($specs, $series); $data = queryfx_all( $conn_r, 'SELECT valueX, epoch FROM %T WHERE factType = %s ORDER BY epoch ASC', $table_name, $series); $points = array(); $sum = 0; foreach ($data as $key => $row) { $sum += (int)$row['valueX']; $points[(int)$row['epoch']] = $sum; } if (!$points) { // NOTE: Raphael crashes Safari if you hand it series with no points. throw new Exception(pht('No data to show!')); } // Limit amount of data passed to browser. $count = count($points); $limit = 2000; if ($count > $limit) { $i = 0; $every = ceil($count / $limit); foreach ($points as $epoch => $sum) { $i++; if ($i % $every && $i != $count) { unset($points[$epoch]); } } } $x = array_keys($points); $y = array_values($points); $id = celerity_generate_unique_node_id(); $chart = phutil_tag( 'div', array( 'id' => $id, 'style' => 'border: 1px solid #6f6f6f; '. 'margin: 1em 2em; '. 'background: #ffffff; '. 'height: 400px; ', ), ''); require_celerity_resource('raphael-core'); require_celerity_resource('raphael-g'); require_celerity_resource('raphael-g-line'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, 'x' => array($x), 'y' => array($y), 'xformat' => 'epoch', 'colors' => array('#0000ff'), )); $panel = new PHUIObjectBoxView(); $panel->setHeaderText(pht('Count of %s', $spec->getName())); $panel->appendChild($chart); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Chart')); return $this->buildApplicationPage( array( $crumbs, $panel, ), array( 'title' => pht('Chart'), )); } } diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php index b0e24cb79f..8e5c9511eb 100644 --- a/src/applications/fact/controller/PhabricatorFactHomeController.php +++ b/src/applications/fact/controller/PhabricatorFactHomeController.php @@ -1,126 +1,125 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); if ($request->isFormPost()) { $uri = new PhutilURI('/fact/chart/'); $uri->setQueryParam('y1', $request->getStr('y1')); return id(new AphrontRedirectResponse())->setURI($uri); } $types = array( '+N:*', '+N:DREV', 'updated', ); $engines = PhabricatorFactEngine::loadAllEngines(); $specs = PhabricatorFactSpec::newSpecsForFactTypes($engines, $types); $facts = id(new PhabricatorFactAggregate())->loadAllWhere( 'factType IN (%Ls)', $types); $rows = array(); foreach ($facts as $fact) { $spec = $specs[$fact->getFactType()]; $name = $spec->getName(); - $value = $spec->formatValueForDisplay($user, $fact->getValueX()); + $value = $spec->formatValueForDisplay($viewer, $fact->getValueX()); $rows[] = array($name, $value); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Fact'), pht('Value'), )); $table->setColumnClasses( array( 'wide', 'n', )); $panel = new PHUIObjectBoxView(); $panel->setHeaderText(pht('Facts')); $panel->setTable($table); $chart_form = $this->buildChartForm(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Home')); return $this->buildApplicationPage( array( $crumbs, $chart_form, $panel, ), array( 'title' => pht('Facts'), )); } private function buildChartForm() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $table = new PhabricatorFactRaw(); $conn_r = $table->establishConnection('r'); $table_name = $table->getTableName(); $facts = queryfx_all( $conn_r, 'SELECT DISTINCT factType from %T', $table_name); $specs = PhabricatorFactSpec::newSpecsForFactTypes( PhabricatorFactEngine::loadAllEngines(), ipull($facts, 'factType')); $options = array(); foreach ($specs as $spec) { if ($spec->getUnit() == PhabricatorFactSpec::UNIT_COUNT) { $options[$spec->getType()] = $spec->getName(); } } if (!$options) { return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NODATA) ->setTitle(pht('No Chartable Facts')) ->appendChild(phutil_tag( 'p', array(), pht('There are no facts that can be plotted yet.'))); } $form = id(new AphrontFormView()) - ->setUser($user) + ->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/controller/PhabricatorFeedDetailController.php b/src/applications/feed/controller/PhabricatorFeedDetailController.php index 459011af80..4fac10d1a8 100644 --- a/src/applications/feed/controller/PhabricatorFeedDetailController.php +++ b/src/applications/feed/controller/PhabricatorFeedDetailController.php @@ -1,50 +1,44 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $story = id(new PhabricatorFeedQuery()) - ->setViewer($user) - ->withChronologicalKeys(array($this->id)) + ->setViewer($viewer) + ->withChronologicalKeys(array($id)) ->executeOne(); if (!$story) { return new Aphront404Response(); } if ($request->getStr('text')) { $text = $story->renderText(); return id(new AphrontPlainTextResponse())->setContent($text); } $feed = array($story); $builder = new PhabricatorFeedBuilder($feed); - $builder->setUser($user); + $builder->setUser($viewer); $feed_view = $builder->buildView(); $title = pht('Story'); $feed_view = phutil_tag_div('phabricator-feed-frame', $feed_view); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $feed_view, ), array( 'title' => $title, )); } } diff --git a/src/applications/feed/controller/PhabricatorFeedListController.php b/src/applications/feed/controller/PhabricatorFeedListController.php index f317b477be..8451592362 100644 --- a/src/applications/feed/controller/PhabricatorFeedListController.php +++ b/src/applications/feed/controller/PhabricatorFeedListController.php @@ -1,24 +1,20 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhabricatorFeedSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/files/controller/PhabricatorFileCommentController.php b/src/applications/files/controller/PhabricatorFileCommentController.php index c527e6fc12..11833ea8d8 100644 --- a/src/applications/files/controller/PhabricatorFileCommentController.php +++ b/src/applications/files/controller/PhabricatorFileCommentController.php @@ -1,68 +1,62 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $file = id(new PhabricatorFileQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $view_uri = $file->getInfoURI(); $xactions = array(); $xactions[] = id(new PhabricatorFileTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorFileTransactionComment()) ->setContent($request->getStr('comment'))); $editor = id(new PhabricatorFileEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($file, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } } diff --git a/src/applications/files/controller/PhabricatorFileComposeController.php b/src/applications/files/controller/PhabricatorFileComposeController.php index f3e450add5..de5a6757a4 100644 --- a/src/applications/files/controller/PhabricatorFileComposeController.php +++ b/src/applications/files/controller/PhabricatorFileComposeController.php @@ -1,341 +1,340 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $colors = array( 'red' => pht('Verbillion'), 'orange' => pht('Navel Orange'), 'yellow' => pht('Prim Goldenrod'), 'green' => pht('Lustrous Verdant'), 'blue' => pht('Tropical Deep'), 'sky' => pht('Wide Open Sky'), 'indigo' => pht('Pleated Khaki'), 'violet' => pht('Aged Merlot'), 'pink' => pht('Easter Bunny'), 'charcoal' => pht('Gemstone'), 'backdrop' => pht('Driven Snow'), ); $manifest = PHUIIconView::getSheetManifest(PHUIIconView::SPRITE_PROJECTS); if ($request->isFormPost()) { $project_phid = $request->getStr('projectPHID'); if ($project_phid) { $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs(array($project_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $icon = $project->getIcon(); $color = $project->getColor(); switch ($color) { case 'grey': $color = 'charcoal'; break; case 'checkered': $color = 'backdrop'; break; } } else { $icon = $request->getStr('icon'); $color = $request->getStr('color'); } if (!isset($colors[$color]) || !isset($manifest['projects-'.$icon])) { return new Aphront404Response(); } $root = dirname(phutil_get_library_root('phabricator')); $icon_file = $root.'/resources/sprite/projects_2x/'.$icon.'.png'; $icon_data = Filesystem::readFile($icon_file); $data = $this->composeImage($color, $icon_data); $file = PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => 'project.png', 'profile' => true, 'canCDN' => true, )); if ($project_phid) { $edit_uri = '/project/profile/'.$project->getID().'/'; $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorProjectTransaction::TYPE_IMAGE) ->setNewValue($file->getPHID()); $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } else { $content = array( 'phid' => $file->getPHID(), ); return id(new AphrontAjaxResponse())->setContent($content); } } $value_color = head_key($colors); $value_icon = head_key($manifest); $value_icon = substr($value_icon, strlen('projects-')); require_celerity_resource('people-profile-css'); $buttons = array(); foreach ($colors as $color => $name) { $buttons[] = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip compose-select-color', 'style' => 'margin: 0 8px 8px 0', 'meta' => array( 'color' => $color, 'tip' => $name, ), ), id(new PHUIIconView()) ->addClass('compose-background-'.$color)); } $sort_these_first = array( 'projects-fa-briefcase', 'projects-fa-tags', 'projects-fa-folder', 'projects-fa-group', 'projects-fa-bug', 'projects-fa-trash-o', 'projects-fa-calendar', 'projects-fa-flag-checkered', 'projects-fa-envelope', 'projects-fa-truck', 'projects-fa-lock', 'projects-fa-umbrella', 'projects-fa-cloud', 'projects-fa-building', 'projects-fa-credit-card', 'projects-fa-flask', ); $manifest = array_select_keys( $manifest, $sort_these_first) + $manifest; $icons = array(); $icon_quips = array( '8ball' => pht('Take a Risk'), 'alien' => pht('Foreign Interface'), 'announce' => pht('Louder is Better'), 'art' => pht('Unique Snowflake'), 'award' => pht('Shooting Star'), 'bacon' => pht('Healthy Vegetables'), 'bandaid' => pht('Durable Infrastructure'), 'beer' => pht('Healthy Vegetable Juice'), 'bomb' => pht('Imminent Success'), 'briefcase' => pht('Adventure Pack'), 'bug' => pht('Costumed Egg'), 'calendar' => pht('Everyone Loves Meetings'), 'cloud' => pht('Water Cycle'), 'coffee' => pht('Half-Whip Nonfat Soy Latte'), 'creditcard' => pht('Expense It'), 'death' => pht('Calcium Promotes Bone Health'), 'desktop' => pht('Magical Portal'), 'dropbox' => pht('Cardboard Box'), 'education' => pht('Debt'), 'experimental' => pht('CAUTION: Dangerous Chemicals'), 'facebook' => pht('Popular Social Network'), 'facility' => pht('Pollution Solves Problems'), 'film' => pht('Actual Physical Film'), 'forked' => pht('You Can\'t Eat Soup'), 'games' => pht('Serious Business'), 'ghost' => pht('Haunted'), 'gift' => pht('Surprise!'), 'globe' => pht('Scanner Sweep'), 'golf' => pht('Business Meeting'), 'heart' => pht('Undergoing a Major Surgery'), 'intergalactic' => pht('Jupiter'), 'lock' => pht('Extremely Secret'), 'mail' => pht('Oragami'), 'martini' => pht('Healthy Olive Drink'), 'medical' => pht('Medic!'), 'mobile' => pht('Cellular Telephone'), 'music' => pht("\xE2\x99\xAB"), 'news' => pht('Actual Physical Newspaper'), 'orgchart' => pht('It\'s Good to be King'), 'peoples' => pht('Angel and Devil'), 'piechart' => pht('Actual Physical Pie'), 'poison' => pht('Healthy Bone Juice'), 'putabirdonit' => pht('Put a Bird On It'), 'radiate' => pht('Radiant Beauty'), 'savings' => pht('Oink Oink'), 'search' => pht('Sleuthing'), 'shield' => pht('Royal Crest'), 'speed' => pht('Slow and Steady'), 'sprint' => pht('Fire Exit'), 'star' => pht('The More You Know'), 'storage' => pht('Stack of Pancakes'), 'tablet' => pht('Cellular Telephone For Giants'), 'travel' => pht('Pretty Clearly an Airplane'), 'twitter' => pht('Bird Stencil'), 'warning' => pht('No Caution Required, Everything Looks Safe'), 'whale' => pht('Friendly Walrus'), 'fa-flask' => pht('Experimental'), 'fa-briefcase' => pht('Briefcase'), 'fa-bug' => pht('Bug'), 'fa-building' => pht('Company'), 'fa-calendar' => pht('Deadline'), 'fa-cloud' => pht('The Cloud'), 'fa-credit-card' => pht('Accounting'), 'fa-envelope' => pht('Communication'), 'fa-flag-checkered' => pht('Goal'), 'fa-folder' => pht('Folder'), 'fa-group' => pht('Team'), 'fa-lock' => pht('Policy'), 'fa-tags' => pht('Tag'), 'fa-trash-o' => pht('Garbage'), 'fa-truck' => pht('Release'), 'fa-umbrella' => pht('An Umbrella'), ); foreach ($manifest as $icon => $spec) { $icon = substr($icon, strlen('projects-')); $icons[] = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip compose-select-icon', 'style' => 'margin: 0 8px 8px 0', 'meta' => array( 'icon' => $icon, 'tip' => idx($icon_quips, $icon, $icon), ), ), id(new PHUIIconView()) ->setSpriteIcon($icon) ->setSpriteSheet(PHUIIconView::SPRITE_PROJECTS)); } $dialog_id = celerity_generate_unique_node_id(); $color_input_id = celerity_generate_unique_node_id(); $icon_input_id = celerity_generate_unique_node_id(); $preview_id = celerity_generate_unique_node_id(); $preview = id(new PHUIIconView()) ->setID($preview_id) ->addClass('compose-background-'.$value_color) ->setSpriteIcon($value_icon) ->setSpriteSheet(PHUIIconView::SPRITE_PROJECTS); $color_input = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'color', 'value' => $value_color, 'id' => $color_input_id, )); $icon_input = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'icon', 'value' => $value_icon, 'id' => $icon_input_id, )); Javelin::initBehavior('phabricator-tooltips'); Javelin::initBehavior( 'icon-composer', array( 'dialogID' => $dialog_id, 'colorInputID' => $color_input_id, 'iconInputID' => $icon_input_id, 'previewID' => $preview_id, 'defaultColor' => $value_color, 'defaultIcon' => $value_icon, )); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setFormID($dialog_id) ->setClass('compose-dialog') ->setTitle(pht('Compose Image')) ->appendChild( phutil_tag( 'div', array( 'class' => 'compose-header', ), pht('Choose Background Color'))) ->appendChild($buttons) ->appendChild( phutil_tag( 'div', array( 'class' => 'compose-header', ), pht('Choose Icon'))) ->appendChild($icons) ->appendChild( phutil_tag( 'div', array( 'class' => 'compose-header', ), pht('Preview'))) ->appendChild($preview) ->appendChild($color_input) ->appendChild($icon_input) ->addCancelButton('/') ->addSubmitButton(pht('Save Image')); return id(new AphrontDialogResponse())->setDialog($dialog); } private function composeImage($color, $icon_data) { $icon_img = imagecreatefromstring($icon_data); $map = id(new CelerityResourceTransformer()) ->getCSSVariableMap(); $color_string = idx($map, $color, '#ff00ff'); $color_const = hexdec(trim($color_string, '#')); $canvas = imagecreatetruecolor(100, 100); imagefill($canvas, 0, 0, $color_const); imagecopy($canvas, $icon_img, 0, 0, 0, 0, 100, 100); return PhabricatorImageTransformer::saveImageDataInAnyFormat( $canvas, 'image/png'); } } diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index e528eb15f2..848560f099 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -1,235 +1,231 @@ phid = $data['phid']; - $this->key = $data['key']; - $this->token = idx($data, 'token'); - } - public function shouldRequireLogin() { return false; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $this->getViewer(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $this->phid = $request->getURIData('phid'); + $this->key = $request->getURIData('key'); + $this->token = $request->getURIData('token'); $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(); $cache_response = true; if (empty($alt) || $main_domain == $alt_domain) { // Alternate files domain isn't configured or it's set // to the same as the default domain $response = $this->loadFile($viewer); if ($response) { return $response; } $file = $this->getFile(); // when the file is not CDNable, don't allow cache $cache_response = $file->getCanCDN(); } else if ($req_domain != $alt_domain) { // Alternate domain is configured but this request isn't using it $response = $this->loadFile($viewer); if ($response) { return $response; } $file = $this->getFile(); // if the user can see the file, generate a token; // redirect to the alt domain with the token; $token_uri = $file->getCDNURIWithToken(); $token_uri = new PhutilURI($token_uri); $token_uri = $this->addURIParameters($token_uri); return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($token_uri); } else { // We are using the alternate domain. We don't have authentication // on this domain, so we bypass policy checks when loading the file. $bypass_policies = PhabricatorUser::getOmnipotentUser(); $response = $this->loadFile($bypass_policies); if ($response) { return $response; } $file = $this->getFile(); $acquire_token_uri = id(new PhutilURI($file->getViewURI())) ->setDomain($main_domain); $acquire_token_uri = $this->addURIParameters($acquire_token_uri); if ($this->token) { // validate the token, if it is valid, continue $validated_token = $file->validateOneTimeToken($this->token); if (!$validated_token) { $dialog = $this->newDialog() ->setShortTitle(pht('Expired File')) ->setTitle(pht('File Link Has Expired')) ->appendParagraph( pht( 'The link you followed to view this file is invalid or '. 'expired.')) ->appendParagraph( pht( 'Continue to generate a new link to the file. You may be '. 'required to log in.')) ->addCancelButton( $acquire_token_uri, pht('Continue')); // Build an explicit response so we can respond with HTTP/403 instead // of HTTP/200. $response = id(new AphrontDialogResponse()) ->setDialog($dialog) ->setHTTPResponseCode(403); return $response; } // return the file data without cache headers $cache_response = false; } else if (!$file->getCanCDN()) { // file cannot be served via cdn, and no token given // redirect to the main domain to aquire a token // This is marked as an "external" URI because it is fully qualified. return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($acquire_token_uri); } } $response = new AphrontFileResponse(); if ($cache_response) { $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); } $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 ($range) { $matches = null; if (preg_match('/^bytes=(\d+)-(\d+)$/', $range, $matches)) { // Note that the "Range" header specifies bytes differently than // we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1). $begin = (int)$matches[1]; $end = (int)$matches[2] + 1; $response->setHTTPResponseCode(206); $response->setRange($begin, ($end - 1)); } } else if (isset($validated_token)) { // We set this on the response, and the response deletes it after the // transfer completes. This allows transfers to be resumed, in theory. $response->setTemporaryFileToken($validated_token); } $is_viewable = $file->isViewableInBrowser(); $force_download = $request->getExists('download'); if ($is_viewable && !$force_download) { $response->setMimeType($file->getViewableMimeType()); } else { if (!$request->isHTTPPost() && !$alt_domain) { // NOTE: Require POST to download files from the primary domain. We'd // rather go full-bore and do a real CSRF check, but can't currently // authenticate users on the file domain. This should blunt any // attacks based on iframes, script tags, applet tags, etc., at least. // Send the user to the "info" page if they're using some other method. // This is marked as "external" because it is fully qualified. return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI(PhabricatorEnv::getProductionURI($file->getBestURI())); } $response->setMimeType($file->getMimeType()); $response->setDownload($file->getName()); } $iterator = $file->getFileDataIterator($begin, $end); $response->setContentLength($file->getByteSize()); $response->setContentIterator($iterator); return $response; } /** * Add passthrough parameters to the URI so they aren't lost when we * redirect to acquire tokens. */ private function addURIParameters(PhutilURI $uri) { $request = $this->getRequest(); if ($request->getBool('download')) { $uri->setQueryParam('download', 1); } return $uri; } private function loadFile(PhabricatorUser $viewer) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } if (!$file->validateSecretKey($this->key)) { return new Aphront403Response(); } if ($file->getIsPartial()) { // 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()); return $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.')) ->addCancelButton($info_uri); } $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/PhabricatorFileDeleteController.php b/src/applications/files/controller/PhabricatorFileDeleteController.php index ed245c5cba..a07fe2e91a 100644 --- a/src/applications/files/controller/PhabricatorFileDeleteController.php +++ b/src/applications/files/controller/PhabricatorFileDeleteController.php @@ -1,51 +1,45 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $file = id(new PhabricatorFileQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$file) { return new Aphront404Response(); } - if (($user->getPHID() != $file->getAuthorPHID()) && - (!$user->getIsAdmin())) { + if (($viewer->getPHID() != $file->getAuthorPHID()) && + (!$viewer->getIsAdmin())) { return new Aphront403Response(); } if ($request->isFormPost()) { $file->delete(); return id(new AphrontRedirectResponse())->setURI('/file/'); } $dialog = new AphrontDialogView(); - $dialog->setUser($user); + $dialog->setUser($viewer); $dialog->setTitle(pht('Really delete file?')); $dialog->appendChild(hsprintf( '

%s

', pht( "Permanently delete '%s'? This action can not be undone.", $file->getName()))); $dialog->addSubmitButton(pht('Delete')); $dialog->addCancelButton($file->getInfoURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/files/controller/PhabricatorFileDropUploadController.php b/src/applications/files/controller/PhabricatorFileDropUploadController.php index 34f6bc81e8..222fc799c7 100644 --- a/src/applications/files/controller/PhabricatorFileDropUploadController.php +++ b/src/applications/files/controller/PhabricatorFileDropUploadController.php @@ -1,123 +1,128 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); // NOTE: Throws if valid CSRF token is not present in the request. $request->validateCSRF(); $name = $request->getStr('name'); $file_phid = $request->getStr('phid'); // If there's no explicit view policy, make it very restrictive by default. // This is the correct policy for files dropped onto objects during // creation, comment and edit flows. $view_policy = $request->getStr('viewPolicy'); if (!$view_policy) { $view_policy = $viewer->getPHID(); } $is_chunks = $request->getBool('querychunks'); if ($is_chunks) { $params = array( 'filePHID' => $file_phid, ); $result = id(new ConduitCall('file.querychunks', $params)) ->setUser($viewer) ->execute(); return id(new AphrontAjaxResponse())->setContent($result); } $is_allocate = $request->getBool('allocate'); if ($is_allocate) { $params = array( 'name' => $name, 'contentLength' => $request->getInt('length'), 'viewPolicy' => $view_policy, ); $result = id(new ConduitCall('file.allocate', $params)) ->setUser($viewer) ->execute(); $file_phid = $result['filePHID']; if ($file_phid) { $file = $this->loadFile($file_phid); $result += $this->getFileDictionary($file); } return id(new AphrontAjaxResponse())->setContent($result); } // Read the raw request data. We're either doing a chunk upload or a // vanilla upload, so we need it. $data = PhabricatorStartup::getRawInput(); $is_chunk_upload = $request->getBool('uploadchunk'); if ($is_chunk_upload) { $params = array( 'filePHID' => $file_phid, 'byteStart' => $request->getInt('byteStart'), 'data' => $data, ); $result = id(new ConduitCall('file.uploadchunk', $params)) ->setUser($viewer) ->execute(); $file = $this->loadFile($file_phid); if ($file->getIsPartial()) { $result = array(); } else { $result = array( 'complete' => true, ) + $this->getFileDictionary($file); } return id(new AphrontAjaxResponse())->setContent($result); } $file = PhabricatorFile::newFromXHRUpload( $data, array( 'name' => $request->getStr('name'), 'authorPHID' => $viewer->getPHID(), 'viewPolicy' => $view_policy, 'isExplicitUpload' => true, )); $result = $this->getFileDictionary($file); return id(new AphrontAjaxResponse())->setContent($result); } private function getFileDictionary(PhabricatorFile $file) { return array( 'id' => $file->getID(), 'phid' => $file->getPHID(), 'uri' => $file->getBestURI(), ); } private function loadFile($file_phid) { $viewer = $this->getViewer(); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception(pht('Failed to load file.')); } return $file; } } diff --git a/src/applications/files/controller/PhabricatorFileEditController.php b/src/applications/files/controller/PhabricatorFileEditController.php index a496202093..9c416b588c 100644 --- a/src/applications/files/controller/PhabricatorFileEditController.php +++ b/src/applications/files/controller/PhabricatorFileEditController.php @@ -1,112 +1,106 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$file) { return new Aphront404Response(); } $title = pht('Edit %s', $file->getName()); $file_name = $file->getName(); $view_uri = '/'.$file->getMonogram(); $error_name = true; $validation_exception = null; if ($request->isFormPost()) { $can_view = $request->getStr('canView'); $file_name = $request->getStr('name'); $errors = array(); $type_name = PhabricatorFileTransaction::TYPE_NAME; $xactions = array(); $xactions[] = id(new PhabricatorFileTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($can_view); $xactions[] = id(new PhabricatorFileTransaction()) ->setTransactionType(PhabricatorFileTransaction::TYPE_NAME) ->setNewValue($file_name); $editor = id(new PhabricatorFileEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($file, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $error_name = $ex->getShortMessage($type_name); $file->setViewPolicy($can_view); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($file) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setValue($file_name) ->setLabel(pht('Name')) ->setError($error_name)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($file) ->setPolicies($policies) ->setName('canView')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($view_uri) ->setValue(pht('Save Changes'))); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($file->getMonogram(), $view_uri) ->addTextCrumb(pht('Edit')); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 0e3d041eac..d9b5de14ee 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -1,382 +1,374 @@ phid = idx($data, 'phid'); - $this->id = idx($data, 'id'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $phid = $request->getURIData('phid'); - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->phid) { + if ($phid) { $file = id(new PhabricatorFileQuery()) - ->setViewer($user) - ->withPHIDs(array($this->phid)) + ->setViewer($viewer) + ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } return id(new AphrontRedirectResponse())->setURI($file->getInfoURI()); } $file = id(new PhabricatorFileQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $phid = $file->getPHID(); $header = id(new PHUIHeaderView()) - ->setUser($user) + ->setUser($viewer) ->setPolicyObject($file) ->setHeader($file->getName()); $ttl = $file->getTTL(); if ($ttl !== null) { $ttl_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_YELLOW) ->setName(pht('Temporary')); $header->addTag($ttl_tag); } $partial = $file->getIsPartial(); if ($partial) { $partial_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_ORANGE) ->setName(pht('Partial Upload')); $header->addTag($partial_tag); } $actions = $this->buildActionView($file); $timeline = $this->buildTransactionView($file); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( 'F'.$file->getID(), $this->getApplicationURI("/info/{$phid}/")); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header); $this->buildPropertyViews($object_box, $file, $actions); return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, ), array( 'title' => $file->getName(), 'pageObjects' => array($file->getPHID()), )); } private function buildTransactionView(PhabricatorFile $file) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $timeline = $this->buildTransactionTimeline( $file, new PhabricatorFileTransactionQuery()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Question File Integrity'); - $draft = PhabricatorDraft::newFromUserAndKey($user, $file->getPHID()); + $draft = PhabricatorDraft::newFromUserAndKey($viewer, $file->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setObjectPHID($file->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$file->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); return array( $timeline, $add_comment_form, ); } private function buildActionView(PhabricatorFile $file) { - $request = $this->getRequest(); - $viewer = $request->getUser(); + $viewer = $this->getViewer(); $id = $file->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $file, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($file); $can_download = !$file->getIsPartial(); if ($file->isViewableInBrowser()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View File')) ->setIcon('fa-file-o') ->setHref($file->getViewURI()) ->setDisabled(!$can_download) ->setWorkflow(!$can_download)); } else { $view->addAction( id(new PhabricatorActionView()) ->setUser($viewer) ->setRenderAsForm($can_download) ->setDownload($can_download) ->setName(pht('Download File')) ->setIcon('fa-download') ->setHref($file->getViewURI()) ->setDisabled(!$can_download) ->setWorkflow(!$can_download)); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit File')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/edit/{$id}/")) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete File')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("/delete/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Transforms')) ->setIcon('fa-crop') ->setHref($this->getApplicationURI("/transforms/{$id}/"))); return $view; } private function buildPropertyViews( PHUIObjectBoxView $box, PhabricatorFile $file, PhabricatorActionListView $actions) { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()); $properties->setActionList($actions); $box->addPropertyList($properties, pht('Details')); if ($file->getAuthorPHID()) { $properties->addProperty( pht('Author'), - $user->renderHandle($file->getAuthorPHID())); + $viewer->renderHandle($file->getAuthorPHID())); } $properties->addProperty( pht('Created'), - phabricator_datetime($file->getDateCreated(), $user)); + phabricator_datetime($file->getDateCreated(), $viewer)); $finfo = id(new PHUIPropertyListView()); $box->addPropertyList($finfo, pht('File Info')); $finfo->addProperty( pht('Size'), phutil_format_bytes($file->getByteSize())); $finfo->addProperty( pht('Mime Type'), $file->getMimeType()); $width = $file->getImageWidth(); if ($width) { $finfo->addProperty( pht('Width'), pht('%s px', new PhutilNumber($width))); } $height = $file->getImageHeight(); if ($height) { $finfo->addProperty( pht('Height'), pht('%s px', new PhutilNumber($height))); } $is_image = $file->isViewableImage(); if ($is_image) { $image_string = pht('Yes'); $cache_string = $file->getCanCDN() ? pht('Yes') : pht('No'); } else { $image_string = pht('No'); $cache_string = pht('Not Applicable'); } $finfo->addProperty(pht('Viewable Image'), $image_string); $finfo->addProperty(pht('Cacheable'), $cache_string); $builtin = $file->getBuiltinName(); if ($builtin === null) { $builtin_string = pht('No'); } else { $builtin_string = $builtin; } $finfo->addProperty(pht('Builtin'), $builtin_string); $is_profile = $file->getIsProfileImage() ? pht('Yes') : pht('No'); $finfo->addProperty(pht('Profile'), $is_profile); $storage_properties = new PHUIPropertyListView(); $box->addPropertyList($storage_properties, pht('Storage')); $storage_properties->addProperty( pht('Engine'), $file->getStorageEngine()); $storage_properties->addProperty( pht('Format'), $file->getStorageFormat()); $storage_properties->addProperty( pht('Handle'), $file->getStorageHandle()); $phids = $file->getObjectPHIDs(); if ($phids) { $attached = new PHUIPropertyListView(); $box->addPropertyList($attached, pht('Attached')); $attached->addProperty( pht('Attached To'), - $user->renderHandleList($phids)); + $viewer->renderHandleList($phids)); } if ($file->isViewableImage()) { $image = phutil_tag( 'img', array( 'src' => $file->getViewURI(), 'class' => 'phui-property-list-image', )); $linked_image = phutil_tag( 'a', array( 'href' => $file->getViewURI(), ), $image); $media = id(new PHUIPropertyListView()) ->addImageContent($linked_image); $box->addPropertyList($media); } else if ($file->isAudio()) { $audio = phutil_tag( 'audio', array( 'controls' => 'controls', 'class' => 'phui-property-list-audio', ), phutil_tag( 'source', array( 'src' => $file->getViewURI(), 'type' => $file->getMimeType(), ))); $media = id(new PHUIPropertyListView()) ->addImageContent($audio); $box->addPropertyList($media); } $engine = null; try { $engine = $file->instantiateStorageEngine(); } catch (Exception $ex) { // Don't bother raising this anywhere for now. } if ($engine) { if ($engine->isChunkEngine()) { $chunkinfo = new PHUIPropertyListView(); $box->addPropertyList($chunkinfo, pht('Chunks')); $chunks = id(new PhabricatorFileChunkQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withChunkHandles(array($file->getStorageHandle())) ->execute(); $chunks = msort($chunks, 'getByteStart'); $rows = array(); $completed = array(); foreach ($chunks as $chunk) { $is_complete = $chunk->getDataFilePHID(); $rows[] = array( $chunk->getByteStart(), $chunk->getByteEnd(), ($is_complete ? pht('Yes') : pht('No')), ); if ($is_complete) { $completed[] = $chunk; } } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Offset'), pht('End'), pht('Complete'), )) ->setColumnClasses( array( '', '', 'wide', )); $chunkinfo->addProperty( pht('Total Chunks'), count($chunks)); $chunkinfo->addProperty( pht('Completed Chunks'), count($completed)); $chunkinfo->addRawContent($table); } } } } diff --git a/src/applications/files/controller/PhabricatorFileUploadDialogController.php b/src/applications/files/controller/PhabricatorFileUploadDialogController.php index e99910c430..dd22caa74a 100644 --- a/src/applications/files/controller/PhabricatorFileUploadDialogController.php +++ b/src/applications/files/controller/PhabricatorFileUploadDialogController.php @@ -1,20 +1,19 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Upload File')) ->appendChild(pht( 'To add files, drag and drop them into the comment text area.')) ->addCancelButton('/', pht('Close')); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/flag/controller/PhabricatorFlagDeleteController.php b/src/applications/flag/controller/PhabricatorFlagDeleteController.php index cf1202c251..4dcb2cc371 100644 --- a/src/applications/flag/controller/PhabricatorFlagDeleteController.php +++ b/src/applications/flag/controller/PhabricatorFlagDeleteController.php @@ -1,29 +1,24 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); - $flag = id(new PhabricatorFlag())->load($this->id); + $flag = id(new PhabricatorFlag())->load($id); if (!$flag) { return new Aphront404Response(); } - if ($flag->getOwnerPHID() != $user->getPHID()) { + if ($flag->getOwnerPHID() != $viewer->getPHID()) { return new Aphront400Response(); } $flag->delete(); return id(new AphrontReloadResponse())->setURI('/flag/'); } } diff --git a/src/applications/flag/controller/PhabricatorFlagEditController.php b/src/applications/flag/controller/PhabricatorFlagEditController.php index b6e2327cd4..5844a3801a 100644 --- a/src/applications/flag/controller/PhabricatorFlagEditController.php +++ b/src/applications/flag/controller/PhabricatorFlagEditController.php @@ -1,93 +1,86 @@ getViewer(); + $phid = $request->getURIData('phid'); - public function willProcessRequest(array $data) { - $this->phid = $data['phid']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $phid = $this->phid; $handle = id(new PhabricatorHandleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$handle->isComplete()) { return new Aphront404Response(); } - $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); + $flag = PhabricatorFlagQuery::loadUserFlag($viewer, $phid); if (!$flag) { $flag = new PhabricatorFlag(); - $flag->setOwnerPHID($user->getPHID()); + $flag->setOwnerPHID($viewer->getPHID()); $flag->setType($handle->getType()); $flag->setObjectPHID($handle->getPHID()); - $flag->setReasonPHID($user->getPHID()); + $flag->setReasonPHID($viewer->getPHID()); } if ($request->isDialogFormPost()) { $flag->setColor($request->getInt('color')); $flag->setNote($request->getStr('note')); $flag->save(); return id(new AphrontReloadResponse())->setURI('/flag/'); } $type_name = $handle->getTypeName(); $dialog = new AphrontDialogView(); - $dialog->setUser($user); + $dialog->setUser($viewer); $dialog->setTitle(pht('Flag %s', $type_name)); require_celerity_resource('phabricator-flag-css'); $form = new PHUIFormLayoutView(); $is_new = !$flag->getID(); if ($is_new) { $form ->appendChild(hsprintf( '

%s


', pht('You can flag this %s if you want to remember to look '. 'at it later.', $type_name))); } $radio = new AphrontFormRadioButtonControl(); foreach (PhabricatorFlagColor::getColorNameMap() as $color => $text) { $class = 'phabricator-flag-radio phabricator-flag-color-'.$color; $radio->addButton($color, $text, '', $class); } $form ->appendChild( $radio ->setName('color') ->setLabel(pht('Flag Color')) ->setValue($flag->getColor())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setName('note') ->setLabel(pht('Note')) ->setValue($flag->getNote())); $dialog->appendChild($form); $dialog->addCancelButton($handle->getURI()); $dialog->addSubmitButton( $is_new ? pht('Create Flag') : pht('Save')); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/flag/controller/PhabricatorFlagListController.php b/src/applications/flag/controller/PhabricatorFlagListController.php index 9029a35a7e..88a6bcf7e0 100644 --- a/src/applications/flag/controller/PhabricatorFlagListController.php +++ b/src/applications/flag/controller/PhabricatorFlagListController.php @@ -1,24 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhabricatorFlagSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php b/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php new file mode 100644 index 0000000000..e4ae03915f --- /dev/null +++ b/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php @@ -0,0 +1,88 @@ +getAdapter()->getPHID(); + $rule = $effect->getRule(); + $author = $rule->getAuthor(); + + $flag = PhabricatorFlagQuery::loadUserFlag($author, $phid); + if ($flag) { + $this->logEffect(self::DO_IGNORE, $flag->getColor()); + return; + } + + $flag = id(new PhabricatorFlag()) + ->setOwnerPHID($author->getPHID()) + ->setType(phid_get_type($phid)) + ->setObjectPHID($phid) + ->setReasonPHID($rule->getPHID()) + ->setColor($effect->getTarget()) + ->setNote('') + ->save(); + + $this->logEffect(self::DO_FLAG, $flag->getColor()); + } + + public function getHeraldActionValueType() { + return id(new HeraldSelectFieldValue()) + ->setKey('flag.color') + ->setOptions(PhabricatorFlagColor::getColorNameMap()) + ->setDefault(PhabricatorFlagColor::COLOR_BLUE); + } + + protected function getActionEffectMap() { + return array( + self::DO_IGNORE => array( + 'icon' => 'fa-times', + 'color' => 'grey', + 'name' => pht('Already Marked'), + ), + self::DO_FLAG => array( + 'icon' => 'fa-flag', + 'name' => pht('Flagged'), + ), + ); + } + + public function renderActionDescription($value) { + $color = PhabricatorFlagColor::getColorName($value); + return pht('Mark with %s flag.', $color); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_IGNORE: + return pht( + 'Already marked with %s flag.', + PhabricatorFlagColor::getColorName($data)); + case self::DO_FLAG: + return pht( + 'Marked with "%s" flag.', + PhabricatorFlagColor::getColorName($data)); + } + } + +} diff --git a/src/applications/fund/controller/FundBackerListController.php b/src/applications/fund/controller/FundBackerListController.php index 0cc0f5a0f5..ab75b123dd 100644 --- a/src/applications/fund/controller/FundBackerListController.php +++ b/src/applications/fund/controller/FundBackerListController.php @@ -1,82 +1,76 @@ id = idx($data, 'id'); - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { - $request = $this->getRequest(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $querykey = $request->getURIData('queryKey'); - if ($this->id) { + if ($id) { $this->initiative = id(new FundInitiativeQuery()) - ->setViewer($request->getUser()) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$this->initiative) { return new Aphront404Response(); } } $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine($this->getEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView() { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $this->getEngine()->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Backers'), $this->getApplicationURI('backers/')); if ($this->initiative) { $crumbs->addTextCrumb( $this->initiative->getMonogram(), '/'.$this->initiative->getMonogram()); } return $crumbs; } private function getEngine() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + $viewer = $this->getViewer(); $engine = id(new FundBackerSearchEngine()) ->setViewer($viewer); if ($this->initiative) { $engine->setInitiative($this->initiative); } return $engine; } } diff --git a/src/applications/fund/controller/FundInitiativeBackController.php b/src/applications/fund/controller/FundInitiativeBackController.php index cb4c12ea40..414a5ebd64 100644 --- a/src/applications/fund/controller/FundInitiativeBackController.php +++ b/src/applications/fund/controller/FundInitiativeBackController.php @@ -1,145 +1,139 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $initiative = id(new FundInitiativeQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$initiative) { return new Aphront404Response(); } $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->withPHIDs(array($initiative->getMerchantPHID())) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $initiative_uri = '/'.$initiative->getMonogram(); if ($initiative->isClosed()) { return $this->newDialog() ->setTitle(pht('Initiative Closed')) ->appendParagraph( pht('You can not back a closed initiative.')) ->addCancelButton($initiative_uri); } $accounts = PhortuneAccountQuery::loadAccountsForUser( $viewer, PhabricatorContentSource::newFromRequest($request)); $v_amount = null; $e_amount = true; $v_account = head($accounts)->getPHID(); $errors = array(); if ($request->isFormPost()) { $v_amount = $request->getStr('amount'); $v_account = $request->getStr('accountPHID'); if (empty($accounts[$v_account])) { $errors[] = pht('You must specify an account.'); } else { $account = $accounts[$v_account]; } if (!strlen($v_amount)) { $errors[] = pht( 'You must specify how much money you want to contribute to the '. 'initiative.'); $e_amount = pht('Required'); } else { try { $currency = PhortuneCurrency::newFromUserInput( $viewer, $v_amount); $currency->assertInRange('1.00 USD', null); } catch (Exception $ex) { $errors[] = $ex->getMessage(); $e_amount = pht('Invalid'); } } if (!$errors) { $backer = FundBacker::initializeNewBacker($viewer) ->setInitiativePHID($initiative->getPHID()) ->attachInitiative($initiative) ->setAmountAsCurrency($currency) ->save(); $product = id(new PhortuneProductQuery()) ->setViewer($viewer) ->withClassAndRef('FundBackerProduct', $initiative->getPHID()) ->executeOne(); $cart_implementation = id(new FundBackerCart()) ->setInitiative($initiative); $cart = $account->newCart($viewer, $cart_implementation, $merchant); $purchase = $cart->newPurchase($viewer, $product); $purchase ->setBasePriceAsCurrency($currency) ->setMetadataValue('backerPHID', $backer->getPHID()) ->save(); $xactions = array(); $xactions[] = id(new FundBackerTransaction()) ->setTransactionType(FundBackerTransaction::TYPE_STATUS) ->setNewValue(FundBacker::STATUS_IN_CART); $editor = id(new FundBackerEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request); $editor->applyTransactions($backer, $xactions); $cart->activateCart(); return id(new AphrontRedirectResponse()) ->setURI($cart->getCheckoutURI()); } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormSelectControl()) ->setName('accountPHID') ->setLabel(pht('Account')) ->setValue($v_account) ->setOptions(mpull($accounts, 'getName', 'getPHID'))) ->appendChild( id(new AphrontFormTextControl()) ->setName('amount') ->setLabel(pht('Amount')) ->setValue($v_amount) ->setError($e_amount)); return $this->newDialog() ->setTitle( pht('Back %s %s', $initiative->getMonogram(), $initiative->getName())) ->setErrors($errors) ->appendChild($form->buildLayoutView()) ->addCancelButton($initiative_uri) ->addSubmitButton(pht('Continue')); } } diff --git a/src/applications/fund/controller/FundInitiativeCloseController.php b/src/applications/fund/controller/FundInitiativeCloseController.php index 803b6242cf..6adddb0d80 100644 --- a/src/applications/fund/controller/FundInitiativeCloseController.php +++ b/src/applications/fund/controller/FundInitiativeCloseController.php @@ -1,75 +1,69 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $initiative = id(new FundInitiativeQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$initiative) { return new Aphront404Response(); } $initiative_uri = '/'.$initiative->getMonogram(); $is_close = !$initiative->isClosed(); if ($request->isFormPost()) { $type_status = FundInitiativeTransaction::TYPE_STATUS; if ($is_close) { $new_status = FundInitiative::STATUS_CLOSED; } else { $new_status = FundInitiative::STATUS_OPEN; } $xaction = id(new FundInitiativeTransaction()) ->setTransactionType($type_status) ->setNewValue($new_status); $editor = id(new FundInitiativeEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true); $editor->applyTransactions($initiative, array($xaction)); return id(new AphrontRedirectResponse())->setURI($initiative_uri); } if ($is_close) { $title = pht('Close Initiative?'); $body = pht( 'Really close this initiative? Users will no longer be able to '. 'back it.'); $button_text = pht('Close Initiative'); } else { $title = pht('Reopen Initiative?'); $body = pht('Really reopen this initiative?'); $button_text = pht('Reopen Initiative'); } return $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addCancelButton($initiative_uri) ->addSubmitButton($button_text); } } diff --git a/src/applications/fund/controller/FundInitiativeEditController.php b/src/applications/fund/controller/FundInitiativeEditController.php index b63b4d6d6b..75f7d338c9 100644 --- a/src/applications/fund/controller/FundInitiativeEditController.php +++ b/src/applications/fund/controller/FundInitiativeEditController.php @@ -1,255 +1,249 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { + if ($id) { $initiative = id(new FundInitiativeQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$initiative) { return new Aphront404Response(); } $is_new = false; } else { $initiative = FundInitiative::initializeNewInitiative($viewer); $is_new = true; } if ($is_new) { $title = pht('Create Initiative'); $button_text = pht('Create Initiative'); $cancel_uri = $this->getApplicationURI(); } else { $title = pht( 'Edit %s %s', $initiative->getMonogram(), $initiative->getName()); $button_text = pht('Save Changes'); $cancel_uri = '/'.$initiative->getMonogram(); } $e_name = true; $v_name = $initiative->getName(); $e_merchant = null; $v_merchant = $initiative->getMerchantPHID(); $v_desc = $initiative->getDescription(); $v_risk = $initiative->getRisks(); if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $initiative->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $validation_exception = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_risk = $request->getStr('risks'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_merchant = $request->getStr('merchantPHID'); $v_projects = $request->getArr('projects'); $type_name = FundInitiativeTransaction::TYPE_NAME; $type_desc = FundInitiativeTransaction::TYPE_DESCRIPTION; $type_risk = FundInitiativeTransaction::TYPE_RISKS; $type_merchant = FundInitiativeTransaction::TYPE_MERCHANT; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $xactions = array(); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_desc) ->setNewValue($v_desc); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_risk) ->setNewValue($v_risk); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_merchant) ->setNewValue($v_merchant); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_view) ->setNewValue($v_view); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_edit) ->setNewValue($v_edit); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new FundInitiativeEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($initiative, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/'.$initiative->getMonogram()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); $e_merchant = $ex->getShortMessage($type_merchant); $initiative->setViewPolicy($v_view); $initiative->setEditPolicy($v_edit); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($initiative) ->execute(); $merchants = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $merchant_options = array(); foreach ($merchants as $merchant) { $merchant_options[$merchant->getPHID()] = pht( 'Merchant %d %s', $merchant->getID(), $merchant->getName()); } if ($v_merchant && empty($merchant_options[$v_merchant])) { $merchant_options = array( $v_merchant => pht('(Restricted Merchant)'), ) + $merchant_options; } if (!$merchant_options) { return $this->newDialog() ->setTitle(pht('No Valid Phortune Merchant Accounts')) ->appendParagraph( pht( 'You do not control any merchant accounts which can receive '. 'payments from this initiative. When you create an initiative, '. 'you need to specify a merchant account where funds will be paid '. 'to.')) ->appendParagraph( pht( 'Create a merchant account in the Phortune application before '. 'creating an initiative in Fund.')) ->addCancelButton($this->getApplicationURI()); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('merchantPHID') ->setLabel(pht('Pay To Merchant')) ->setValue($v_merchant) ->setError($e_merchant) ->setOptions($merchant_options)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('risks') ->setLabel(pht('Risks/Challenges')) ->setValue($v_risk)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($initiative) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($initiative) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button_text) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $crumbs->addTextCrumb(pht('Create Initiative')); } else { $crumbs->addTextCrumb( $initiative->getMonogram(), '/'.$initiative->getMonogram()); $crumbs->addTextCrumb(pht('Edit')); } $box = id(new PHUIObjectBoxView()) ->setValidationException($validation_exception) ->setHeaderText($title) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/fund/controller/FundInitiativeListController.php b/src/applications/fund/controller/FundInitiativeListController.php index c737de4828..6ba398fb2b 100644 --- a/src/applications/fund/controller/FundInitiativeListController.php +++ b/src/applications/fund/controller/FundInitiativeListController.php @@ -1,60 +1,56 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new FundInitiativeSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView() { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new FundInitiativeSearchEngine()) - ->setViewer($user) + ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->addLabel(pht('Backers')); $nav->addFilter('backers/', pht('Find Backers')); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $can_create = $this->hasApplicationCapability( FundCreateInitiativesCapability::CAPABILITY); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Initiative')) ->setHref($this->getApplicationURI('create/')) ->setIcon('fa-plus-square') ->setDisabled(!$can_create) ->setWorkflow(!$can_create)); return $crumbs; } } diff --git a/src/applications/fund/controller/FundInitiativeViewController.php b/src/applications/fund/controller/FundInitiativeViewController.php index 37618b22d2..3d12f1549c 100644 --- a/src/applications/fund/controller/FundInitiativeViewController.php +++ b/src/applications/fund/controller/FundInitiativeViewController.php @@ -1,183 +1,178 @@ id = $data['id']; - } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $initiative = id(new FundInitiativeQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$initiative) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($initiative->getMonogram()); $title = pht( '%s %s', $initiative->getMonogram(), $initiative->getName()); if ($initiative->isClosed()) { $status_icon = 'fa-times'; $status_color = 'bluegrey'; } else { $status_icon = 'fa-check'; $status_color = 'bluegrey'; } $status_name = idx( FundInitiative::getStatusNameMap(), $initiative->getStatus()); $header = id(new PHUIHeaderView()) ->setHeader($initiative->getName()) ->setUser($viewer) ->setPolicyObject($initiative) ->setStatus($status_icon, $status_color, $status_name); $properties = $this->buildPropertyListView($initiative); $actions = $this->buildActionListView($initiative); $properties->setActionList($actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $timeline = $this->buildTransactionTimeline( $initiative, new FundInitiativeTransactionQuery()); $timeline ->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, ), array( 'title' => $title, 'pageObjects' => array($initiative->getPHID()), )); } private function buildPropertyListView(FundInitiative $initiative) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($initiative); $owner_phid = $initiative->getOwnerPHID(); $merchant_phid = $initiative->getMerchantPHID(); $view->addProperty( pht('Owner'), $viewer->renderHandle($owner_phid)); $view->addProperty( pht('Payable to Merchant'), $viewer->renderHandle($merchant_phid)); $view->addProperty( pht('Total Funding'), $initiative->getTotalAsCurrency()->formatForDisplay()); $view->invokeWillRenderEvent(); $description = $initiative->getDescription(); if (strlen($description)) { $description = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($description), 'default', $viewer); $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } $risks = $initiative->getRisks(); if (strlen($risks)) { $risks = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($risks), 'default', $viewer); $view->addSectionHeader(pht('Risks/Challenges')); $view->addTextContent($risks); } return $view; } private function buildActionListView(FundInitiative $initiative) { $viewer = $this->getRequest()->getUser(); $id = $initiative->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $initiative, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($initiative); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Initiative')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($this->getApplicationURI("/edit/{$id}/"))); if ($initiative->isClosed()) { $close_name = pht('Reopen Initiative'); $close_icon = 'fa-check'; } else { $close_name = pht('Close Initiative'); $close_icon = 'fa-times'; } $view->addAction( id(new PhabricatorActionView()) ->setName($close_name) ->setIcon($close_icon) ->setDisabled(!$can_edit) ->setWorkflow(true) ->setHref($this->getApplicationURI("/close/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Back Initiative')) ->setIcon('fa-money') ->setDisabled($initiative->isClosed()) ->setWorkflow(true) ->setHref($this->getApplicationURI("/back/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Backers')) ->setIcon('fa-bank') ->setHref($this->getApplicationURI("/backers/{$id}/"))); return $view; } } diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php index 016e2a5cbc..266ac4fafc 100644 --- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php +++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php @@ -1,99 +1,104 @@ pht('Harbormaster User Guide'), + 'href' => PhabricatorEnv::getDoclink('Harbormaster User Guide'), + ), + ); + } + public function getRoutes() { return array( '/B(?P[1-9]\d*)' => 'HarbormasterBuildableViewController', '/harbormaster/' => array( '(?:query/(?P[^/]+)/)?' => 'HarbormasterBuildableListController', 'step/' => array( 'add/(?:(?P\d+)/)?' => 'HarbormasterStepAddController', 'new/(?P\d+)/(?P[^/]+)/' => 'HarbormasterStepEditController', 'edit/(?:(?P\d+)/)?' => 'HarbormasterStepEditController', 'delete/(?:(?P\d+)/)?' => 'HarbormasterStepDeleteController', ), 'buildable/' => array( '(?P\d+)/(?Pstop|resume|restart)/' => 'HarbormasterBuildableActionController', ), 'build/' => array( '(?P\d+)/' => 'HarbormasterBuildViewController', '(?Pstop|resume|restart)/(?P\d+)/(?:(?P[^/]+)/)?' => 'HarbormasterBuildActionController', ), 'plan/' => array( '(?:query/(?P[^/]+)/)?' => 'HarbormasterPlanListController', 'edit/(?:(?P\d+)/)?' => 'HarbormasterPlanEditController', 'order/(?:(?P\d+)/)?' => 'HarbormasterPlanOrderController', 'disable/(?P\d+)/' => 'HarbormasterPlanDisableController', 'run/(?P\d+)/' => 'HarbormasterPlanRunController', '(?P\d+)/' => 'HarbormasterPlanViewController', ), 'unit/' => array( '(?P\d+)/' => 'HarbormasterUnitMessagesController', ), 'lint/' => array( '(?P\d+)/' => 'HarbormasterLintMessagesController', ), ), ); } protected function getCustomCapabilities() { return array( HarbormasterManagePlansCapability::CAPABILITY => array( 'caption' => pht('Can create and manage build plans.'), 'default' => PhabricatorPolicies::POLICY_ADMIN, ), ); } } diff --git a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php index 9590f90cec..3a2d808239 100644 --- a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php +++ b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php @@ -1,82 +1,268 @@ $parameter) { + $type = idx($parameter, 'type'); + $type = str_replace('|', pht(' or '), $type); + $description = idx($parameter, 'description'); + $rows[] = "| `{$key}` | //{$type}// | {$description} |"; + } + $unit_table = implode("\n", $rows); + + $rows = array(); + $rows[] = "| {$head_key} | {$head_name} | {$head_desc} |"; + $rows[] = '|-------------|--------------|--------------|'; + $results = ArcanistUnitTestResult::getAllResultCodes(); + foreach ($results as $result_code) { + $name = ArcanistUnitTestResult::getResultCodeName($result_code); + $description = ArcanistUnitTestResult::getResultCodeDescription( + $result_code); + $rows[] = "| `{$result_code}` | **{$name}** | {$description} |"; + } + $result_table = implode("\n", $rows); + + $rows = array(); + $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |"; + $rows[] = '|-------------|--------------|--------------|'; + $lint_spec = HarbormasterBuildLintMessage::getParameterSpec(); + foreach ($lint_spec as $key => $parameter) { + $type = idx($parameter, 'type'); + $type = str_replace('|', pht(' or '), $type); + $description = idx($parameter, 'description'); + $rows[] = "| `{$key}` | //{$type}// | {$description} |"; + } + $lint_table = implode("\n", $rows); + + $rows = array(); + $rows[] = "| {$head_key} | {$head_name} |"; + $rows[] = '|-------------|--------------|'; + $severities = ArcanistLintSeverity::getLintSeverities(); + foreach ($severities as $key => $name) { + $rows[] = "| `{$key}` | **{$name}** |"; + } + $severity_table = implode("\n", $rows); + + $valid_unit = array( + array( + 'name' => 'PassingTest', + 'result' => ArcanistUnitTestResult::RESULT_PASS, + ), + array( + 'name' => 'FailingTest', + 'result' => ArcanistUnitTestResult::RESULT_FAIL, + ), + ); + + $valid_lint = array( + array( + 'name' => pht('Syntax Error'), + 'code' => 'EXAMPLE1', + 'severity' => ArcanistLintSeverity::SEVERITY_ERROR, + 'path' => 'path/to/example.c', + 'line' => 17, + 'char' => 3, + ), + array( + 'name' => pht('Not A Haiku'), + 'code' => 'EXAMPLE2', + 'severity' => ArcanistLintSeverity::SEVERITY_ERROR, + 'path' => 'path/to/source.cpp', + 'line' => 23, + 'char' => 1, + 'description' => pht( + 'This function definition is not a haiku.'), + ), + ); + + $json = new PhutilJSON(); + $valid_unit = $json->encodeAsList($valid_unit); + $valid_lint = $json->encodeAsList($valid_lint); + return pht( - 'Send a message to a build target, notifying it of results in an '. - 'external system.'); + "Send a message about the status of a build target to Harbormaster, ". + "notifying the application of build results in an external system.". + "\n\n". + "Sending Messages\n". + "================\n". + "If you run external builds, you can use this method to publish build ". + "results back into Harbormaster after the external system finishes work ". + "or as it makes progress.". + "\n\n". + "The simplest way to use this method is to call it once after the ". + "build finishes with a `pass` or `fail` message. This will record the ". + "build result, and continue the next step in the build if the build was ". + "waiting for a result.". + "\n\n". + "When you send a status message about a build target, you can ". + "optionally include detailed `lint` or `unit` results alongside the ". + "message. See below for details.". + "\n\n". + "If you want to report intermediate results but a build hasn't ". + "completed yet, you can use the `work` message. This message doesn't ". + "have any direct effects, but allows you to send additional data to ". + "update the progress of the build target. The target will continue ". + "waiting for a completion message, but the UI will update to show the ". + "progress which has been made.". + "\n\n". + "Message Types\n". + "=============\n". + "When you send Harbormaster a message, you must include a `type`, ". + "which describes the overall state of the build. For example, use ". + "`pass` to tell Harbomaster that a build completed successfully.". + "\n\n". + "Supported message types are:". + "\n\n". + "%s". + "\n\n". + "Unit Results\n". + "============\n". + "You can report test results alongside a message. The simplest way to ". + "do this is to report all the results alongside a `pass` or `fail` ". + "message, but you can also send a `work` message to report intermediate ". + "results.\n\n". + "To provide unit test results, pass a list of results in the `unit` ". + "parameter. Each result shoud be a dictionary with these keys:". + "\n\n". + "%s". + "\n\n". + "The `result` parameter recognizes these test results:". + "\n\n". + "%s". + "\n\n". + "This is a simple, valid value for the `unit` parameter. It reports ". + "one passing test and one failing test:\n\n". + "\n\n". + "```lang=json\n". + "%s". + "```". + "\n\n". + "Lint Results\n". + "============\n". + "Like unit test results, you can report lint results alongside a ". + "message. The `lint` parameter should contain results as a list of ". + "dictionaries with these keys:". + "\n\n". + "%s". + "\n\n". + "The `severity` parameter recognizes these severity levels:". + "\n\n". + "%s". + "\n\n". + "This is a simple, valid value for the `lint` parameter. It reports one ". + "error and one warning:". + "\n\n". + "```lang=json\n". + "%s". + "```". + "\n\n", + $message_table, + $unit_table, + $result_table, + $valid_unit, + $lint_table, + $severity_table, + $valid_lint); } protected function defineParamTypes() { - $type_const = $this->formatStringConstants(array('pass', 'fail')); + $messages = HarbormasterMessageType::getAllMessages(); + $type_const = $this->formatStringConstants($messages); return array( 'buildTargetPHID' => 'required phid', - 'lint' => 'optional list', - 'unit' => 'optional list', 'type' => 'required '.$type_const, + 'unit' => 'optional list', + 'lint' => 'optional list', ); } protected function defineReturnType() { return 'void'; } protected function execute(ConduitAPIRequest $request) { $viewer = $request->getUser(); $build_target_phid = $request->getValue('buildTargetPHID'); $message_type = $request->getValue('type'); $build_target = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->withPHIDs(array($build_target_phid)) ->executeOne(); if (!$build_target) { throw new Exception(pht('No such build target!')); } $save = array(); $lint_messages = $request->getValue('lint', array()); foreach ($lint_messages as $lint) { $save[] = HarbormasterBuildLintMessage::newFromDictionary( $build_target, $lint); } $unit_messages = $request->getValue('unit', array()); foreach ($unit_messages as $unit) { $save[] = HarbormasterBuildUnitMessage::newFromDictionary( $build_target, $unit); } $save[] = HarbormasterBuildMessage::initializeNewMessage($viewer) ->setBuildTargetPHID($build_target->getPHID()) ->setType($message_type); $build_target->openTransaction(); foreach ($save as $object) { $object->save(); } $build_target->saveTransaction(); // If the build has completely paused because all steps are blocked on // waiting targets, this will resume it. PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build_target->getBuild()->getID(), )); return null; } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 417b9a700e..da35e389a0 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -1,492 +1,492 @@ 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); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($plan); $this->buildPropertyLists($box, $plan, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Plan %d', $id)); list($step_list, $has_any_conflicts, $would_deadlock) = $this->buildStepList($plan); if ($would_deadlock) { $box->setFormErrors( array( 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. $box->setFormErrors( array( pht( 'This build plan has conflicts in one or more build steps. '. 'Examine the step list and resolve the listed errors.'), )); } return $this->buildApplicationPage( array( $crumbs, $box, $step_list, $timeline, ), array( 'title' => $title, )); } 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'); $has_manage = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $has_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $plan, PhabricatorPolicyCapability::CAN_EDIT); $can_edit = ($has_manage && $has_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++; } $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 = id(new PHUIObjectItemView()) ->setObjectName(pht('Step %d.%d', $depth, $i)) ->setHeader(pht('Unknown Implementation')) ->setStatusIcon('fa-warning red') ->addAttribute(pht( 'This step has an invalid implementation (%s).', $step->getClassName())) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setRenderNameAsTooltip(true) ->setName(pht('Delete')) ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); $step_list->addItem($item); continue; } $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Step %d.%d', $depth, $i)) ->setHeader($step->getName()); $item->addAttribute($implementation->getDescription()); $step_id = $step->getID(); $edit_uri = $this->getApplicationURI("step/edit/{$step_id}/"); $delete_uri = $this->getApplicationURI("step/delete/{$step_id}/"); if ($can_edit) { $item->setHref($edit_uri); } $item ->setHref($edit_uri) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setDisabled(!$can_edit) ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); $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->addItem($item); } $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( id(new PHUIIconView()) ->setIconFont('fa-plus')) ->setDisabled(!$can_edit) - ->setWorkflow(true)); + ->setWorkflow(!$can_edit)); $step_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($step_list); return array($step_box, $has_any_conflicts, $is_deadlocking); } private function buildActionList(HarbormasterBuildPlan $plan) { $viewer = $this->getViewer(); $id = $plan->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($plan) ->setObjectURI($this->getApplicationURI("plan/{$id}/")); $has_manage = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $has_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $plan, PhabricatorPolicyCapability::CAN_EDIT); $can_edit = ($has_manage && $has_edit); $list->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()) { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Enable Plan')) ->setHref($this->getApplicationURI("plan/disable/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-check')); } else { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Plan')) ->setHref($this->getApplicationURI("plan/disable/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-ban')); } $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Run Plan Manually')) ->setHref($this->getApplicationURI("plan/run/{$id}/")) ->setWorkflow(true) ->setDisabled(!$has_manage) ->setIcon('fa-play-circle')); return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuildPlan $plan, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($plan) ->setActionList($actions); $box->addPropertyList($properties); $properties->addProperty( pht('Created'), phabricator_datetime($plan->getDateCreated(), $viewer)); } 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) { 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); } } diff --git a/src/applications/harbormaster/controller/HarbormasterStepAddController.php b/src/applications/harbormaster/controller/HarbormasterStepAddController.php index 371a1d1db2..21cdc3f12c 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepAddController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepAddController.php @@ -1,70 +1,105 @@ getViewer(); $this->requireApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $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_id = $plan->getID(); $cancel_uri = $this->getApplicationURI("plan/{$plan_id}/"); + $plan_title = pht('Plan %d', $plan_id); $all = HarbormasterBuildStepImplementation::getImplementations(); - foreach ($all as $key => $impl) { - if ($impl->shouldRequireAutotargeting()) { - unset($all[$key]); + $all = msort($all, 'getName'); + + $all_groups = HarbormasterBuildStepGroup::getAllGroups(); + foreach ($all as $impl) { + $group_key = $impl->getBuildStepGroupKey(); + if (empty($all_groups[$group_key])) { + throw new Exception( + pht( + 'Build step "%s" has step group key "%s", but no step group '. + 'with that key exists.', + get_class($impl), + $group_key)); } } - $errors = array(); - if ($request->isFormPost()) { - $class = $request->getStr('class'); - if (empty($all[$class])) { - $errors[] = pht('Choose the type of build step you want to add.'); + $groups = mgroup($all, 'getBuildStepGroupKey'); + $lists = array(); + + $enabled_groups = HarbormasterBuildStepGroup::getAllEnabledGroups(); + foreach ($enabled_groups as $group) { + $list = id(new PHUIObjectItemListView()) + ->setHeader($group->getGroupName()) + ->setNoDataString( + pht( + 'This group has no available build steps.')); + + $steps = idx($groups, $group->getGroupKey(), array()); + + foreach ($steps as $key => $impl) { + if ($impl->shouldRequireAutotargeting()) { + unset($steps[$key]); + continue; + } } - if (!$errors) { - $new_uri = $this->getApplicationURI("step/new/{$plan_id}/{$class}/"); - return id(new AphrontRedirectResponse())->setURI($new_uri); + + if (!$steps && !$group->shouldShowIfEmpty()) { + continue; } - } - $control = id(new AphrontFormRadioButtonControl()) - ->setName('class'); + foreach ($steps as $key => $impl) { + $class = get_class($impl); - foreach ($all as $class => $implementation) { - $control->addButton( - $class, - $implementation->getName(), - $implementation->getGenericDescription()); - } + $new_uri = $this->getApplicationURI("step/new/{$plan_id}/{$class}/"); - if ($errors) { - $errors = id(new PHUIInfoView()) - ->setErrors($errors); + $item = id(new PHUIObjectItemView()) + ->setHeader($impl->getName()) + ->setHref($new_uri) + ->addAttribute($impl->getGenericDescription()); + + $list->addItem($item); + } + + $lists[] = $list; } - return $this->newDialog() - ->setTitle(pht('Add New Step')) - ->addSubmitButton(pht('Add Build Step')) - ->addCancelButton($cancel_uri) - ->appendChild($errors) - ->appendParagraph(pht('Choose a type of build step to add:')) - ->appendChild($control); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($plan_title, $cancel_uri) + ->addTextCrumb(pht('Add Build Step')); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Add Build Step')) + ->appendChild($lists); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => array( + $plan_title, + pht('Add Build Step'), + ), + )); } } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 8d8c26818e..fd895979cd 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -1,488 +1,491 @@ 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(HarbormasterBuild::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(), )); } // 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()); } // 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->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) || ($build->isRestarting())) { $this->restartBuild($build); $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); $build->save(); } if ($build->isResuming()) { $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); $build->save(); } if ($build->isStopping() && !$build->isComplete()) { $build->setBuildStatus(HarbormasterBuild::STATUS_STOPPED); $build->save(); } $build->deleteUnprocessedCommands(); if ($build->getBuildStatus() == HarbormasterBuild::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) { $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($build->getBuildGeneration())) ->execute(); $this->updateWaitingTargets($targets); $targets = mgroup($targets, 'getBuildStepPHID'); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($this->getViewer()) ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) ->execute(); // 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(HarbormasterBuild::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(HarbormasterBuild::STATUS_PASSED); $build->save(); return; } // 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(HarbormasterBuild::STATUS_DEADLOCKED); $build->save(); return; } foreach ($runnable as $runnable_step) { $target = HarbormasterBuildTarget::initializeNewBuildTarget( $build, $runnable_step, $build->retrieveVariablesFromBuild()); $target->save(); $this->queueNewBuildTarget($target); } } /** * Process messages which were sent to these targets, kicking applicable * targets out of "Waiting" and into either "Passed" or "Failed". * * @param list 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()) ->withBuildTargetPHIDs(array_keys($waiting_targets)) ->withConsumed(false) ->execute(); foreach ($messages as $message) { $target = $waiting_targets[$message->getBuildTargetPHID()]; - $new_status = null; switch ($message->getType()) { - case 'pass': + case HarbormasterMessageType::MESSAGE_PASS: $new_status = HarbormasterBuildTarget::STATUS_PASSED; break; - case 'fail': + 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 */ private 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(); $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { if ($build->getBuildStatus() != HarbormasterBuild::STATUS_PASSED) { $all_pass = false; } if ($build->getBuildStatus() == HarbormasterBuild::STATUS_FAILED || $build->getBuildStatus() == HarbormasterBuild::STATUS_ERROR || $build->getBuildStatus() == HarbormasterBuild::STATUS_DEADLOCKED) { $any_fail = true; } } if ($any_fail) { $new_status = HarbormasterBuildable::STATUS_FAILED; } else if ($all_pass) { $new_status = HarbormasterBuildable::STATUS_PASSED; } else { $new_status = HarbormasterBuildable::STATUS_BUILDING; } $old_status = $buildable->getBuildableStatus(); $did_update = ($old_status != $new_status); if ($did_update) { $buildable->setBuildableStatus($new_status); $buildable->save(); } $lock->unlock(); // If we changed the buildable status, try to post a transaction to the // object about it. We can safely do this outside of the locked region. // NOTE: We only post transactions for automatic buildables, not for // manual ones: manual builds are test builds, whoever is doing tests // can look at the results themselves, and other users generally don't // care about the outcome. $should_publish = $did_update && $new_status != HarbormasterBuildable::STATUS_BUILDING && !$buildable->getIsManualBuildable(); if (!$should_publish) { return; } $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($buildable->getBuildablePHID())) ->executeOne(); if (!$object) { return; } if (!($object instanceof PhabricatorApplicationTransactionInterface)) { return; } // TODO: Publishing these transactions is causing a race. See T8650. // We shouldn't be publishing to diffs anyway. if ($object instanceof DifferentialDiff) { return; } $template = $object->getApplicationTransactionTemplate(); if (!$template) { return; } $template ->setTransactionType(PhabricatorTransactions::TYPE_BUILDABLE) ->setMetadataValue( 'harbormaster:buildablePHID', $buildable->getPHID()) ->setOldValue($old_status) ->setNewValue($new_status); $harbormaster_phid = id(new PhabricatorHarbormasterApplication()) ->getPHID(); $daemon_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_DAEMON, array()); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($harbormaster_phid) ->setContentSource($daemon_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $editor->applyTransactions( $object->getApplicationTransactionObject(), array($template)); } 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) ->execute(); foreach ($artifacts as $artifact) { $artifact->release(); } } } diff --git a/src/applications/harbormaster/engine/HarbormasterMessageType.php b/src/applications/harbormaster/engine/HarbormasterMessageType.php new file mode 100644 index 0000000000..5900817c69 --- /dev/null +++ b/src/applications/harbormaster/engine/HarbormasterMessageType.php @@ -0,0 +1,44 @@ + array( + 'description' => pht( + 'Report that the target is complete, and the target has passed.'), + ), + self::MESSAGE_FAIL => array( + 'description' => pht( + 'Report that the target is complete, and the target has failed.'), + ), + self::MESSAGE_WORK => array( + 'description' => pht( + 'Report that work on the target is ongoing. This message can be '. + 'used to report partial results during a build.'), + ), + ); + } + +} diff --git a/src/applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php b/src/applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php new file mode 100644 index 0000000000..cf6cda8f95 --- /dev/null +++ b/src/applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php @@ -0,0 +1,34 @@ +getObject()->getPHID(); + } + + public function getHarbormasterContainerPHID() { + return null; + } + + public function getQueuedHarbormasterBuildPlanPHIDs() { + return $this->buildPlanPHIDs; + } + + public function queueHarbormasterBuildPlanPHID($phid) { + $this->buildPlanPHIDs[] = $phid; + } + +*/ diff --git a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php new file mode 100644 index 0000000000..830598e812 --- /dev/null +++ b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php @@ -0,0 +1,85 @@ +getAdapter(); + return ($adapter instanceof HarbormasterBuildableAdapterInterface); + } + + protected function applyBuilds(array $phids) { + $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) { + $adapter->queueHarbormasterBuildPlanPHID($phid); + } + + $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.', + new PhutilNumber(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()); + } + + 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)); + } +} diff --git a/src/applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php index 93b6e498c7..ed69ab807d 100644 --- a/src/applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php @@ -1,38 +1,42 @@ setAncestorClass(__CLASS__) ->execute(); } public static function getImplementation($class) { $base = idx(self::getImplementations(), $class); if ($base) { return (clone $base); } return null; } public static function requireImplementation($class) { if (!$class) { throw new Exception(pht('No implementation is specified!')); } $implementation = self::getImplementation($class); if (!$implementation) { throw new Exception(pht('No such implementation "%s" exists!', $class)); } return $implementation; } /** * The name of the implementation. */ abstract public function getName(); + public function getBuildStepGroupKey() { + return HarbormasterOtherBuildStepGroup::GROUPKEY; + } + /** * The generic description of the implementation. */ public function getGenericDescription() { return ''; } /** * The description of the implementation, based on the current settings. */ public function getDescription() { return $this->getGenericDescription(); } /** * Run the build target against the specified build. */ abstract public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. */ public function getSettings() { return $this->settings; } public function getSetting($key, $default = null) { return idx($this->settings, $key, $default); } /** * Loads the settings for this build step implementation from a build * step or target. */ final public function loadSettings($build_object) { $this->settings = $build_object->getDetails(); return $this; } /** * Return the name of artifacts produced by this command. * * Something like: * * return array( * 'some_name_input_by_user' => 'host'); * * Future steps will calculate all available artifact mappings * before them and filter on the type. * * @return array The mappings of artifact names to their types. */ public function getArtifactInputs() { return array(); } public function getArtifactOutputs() { return array(); } public function getDependencies(HarbormasterBuildStep $build_step) { $dependencies = $build_step->getDetail('dependsOn', array()); $inputs = $build_step->getStepImplementation()->getArtifactInputs(); $inputs = ipull($inputs, null, 'key'); $artifacts = $this->getAvailableArtifacts( $build_step->getBuildPlan(), $build_step, null); foreach ($artifacts as $key => $type) { if (!array_key_exists($key, $inputs)) { unset($artifacts[$key]); } } $artifact_steps = ipull($artifacts, 'step'); $artifact_steps = mpull($artifact_steps, 'getPHID'); $dependencies = array_merge($dependencies, $artifact_steps); return $dependencies; } /** * Returns a list of all artifacts made available in the build plan. */ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, $current_build_step, $artifact_type) { $steps = id(new HarbormasterBuildStepQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($build_plan->getPHID())) ->execute(); $artifacts = array(); $artifact_arrays = array(); foreach ($steps as $step) { if ($current_build_step !== null && $step->getPHID() === $current_build_step->getPHID()) { continue; } $implementation = $step->getStepImplementation(); $array = $implementation->getArtifactOutputs(); $array = ipull($array, 'type', 'key'); foreach ($array as $name => $type) { if ($type !== $artifact_type && $artifact_type !== null) { continue; } $artifacts[$name] = array('type' => $type, 'step' => $step); } } return $artifacts; } /** * Convert a user-provided string with variables in it, like: * * ls ${dirname} * * ...into a string with variables merged into it safely: * * ls 'dir with spaces' * * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}. * @param string User-provided pattern string containing `${variables}`. * @param dict List of available replacement variables. * @return string String with variables replaced safely into it. */ protected function mergeVariables($function, $pattern, array $variables) { $regexp = '/\\$\\{(?P[a-z\\.]+)\\}/'; $matches = null; preg_match_all($regexp, $pattern, $matches); $argv = array(); foreach ($matches['name'] as $name) { if (!array_key_exists($name, $variables)) { throw new Exception(pht("No such variable '%s'!", $name)); } $argv[] = $variables[$name]; } $pattern = str_replace('%', '%%', $pattern); $pattern = preg_replace($regexp, '%s', $pattern); return call_user_func($function, $pattern, $argv); } public function getFieldSpecifications() { return array(); } protected function formatSettingForDescription($key, $default = null) { return $this->formatValueForDescription($this->getSetting($key, $default)); } protected function formatValueForDescription($value) { if (strlen($value)) { return phutil_tag('strong', array(), $value); } else { return phutil_tag('em', array(), pht('(null)')); } } public function supportsWaitForMessage() { return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { if (!$this->supportsWaitForMessage()) { return false; } return (bool)$target->getDetail('builtin.wait-for-message'); } protected function shouldAbort( HarbormasterBuild $build, HarbormasterBuildTarget $target) { return $build->getBuildGeneration() !== $target->getBuildGeneration(); } protected function resolveFuture( HarbormasterBuild $build, HarbormasterBuildTarget $target, Future $future) { $futures = new FutureIterator(array($future)); foreach ($futures->setUpdateInterval(5) as $key => $future) { if ($future === null) { $build->reload(); if ($this->shouldAbort($build, $target)) { throw new HarbormasterBuildAbortedException(); } } else { return $future->resolve(); } } } /* -( Automatic Targets )-------------------------------------------------- */ public function getBuildStepAutotargetStepKey() { return null; } public function getBuildStepAutotargetPlanKey() { throw new PhutilMethodNotImplementedException(); } public function shouldRequireAutotargeting() { return false; } } diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php index 5dbfd80262..931ade75a7 100644 --- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php @@ -1,144 +1,148 @@ formatSettingForDescription('command'), $this->formatSettingForDescription('hostartifact')); } public function escapeCommand($pattern, array $args) { array_unshift($args, $pattern); $mode = PhutilCommandString::MODE_DEFAULT; if ($this->platform == 'windows') { $mode = PhutilCommandString::MODE_POWERSHELL; } return id(new PhutilCommandString($args)) ->setEscapingMode($mode); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $artifact = $build->loadArtifact($settings['hostartifact']); $lease = $artifact->loadDrydockLease(); $this->platform = $lease->getAttribute('platform'); $command = $this->mergeVariables( array($this, 'escapeCommand'), $settings['command'], $variables); $this->platform = null; $interface = $lease->getInterface('command'); $future = $interface->getExecFuture('%C', $command); $log_stdout = $build->createLog($build_target, 'remote', 'stdout'); $log_stderr = $build->createLog($build_target, 'remote', 'stderr'); $start_stdout = $log_stdout->start(); $start_stderr = $log_stderr->start(); $build_update = 5; // Read the next amount of available output every second. $futures = new FutureIterator(array($future)); foreach ($futures->setUpdateInterval(1) as $key => $future_iter) { if ($future_iter === null) { // Check to see if we should abort. if ($build_update <= 0) { $build->reload(); if ($this->shouldAbort($build, $build_target)) { $future->resolveKill(); throw new HarbormasterBuildAbortedException(); } else { $build_update = 5; } } else { $build_update -= 1; } // Command is still executing. // Read more data as it is available. list($stdout, $stderr) = $future->read(); $log_stdout->append($stdout); $log_stderr->append($stderr); $future->discardBuffers(); } else { // Command execution is complete. // Get the return value so we can log that as well. list($err) = $future->resolve(); // Retrieve the last few bits of information. list($stdout, $stderr) = $future->read(); $log_stdout->append($stdout); $log_stderr->append($stderr); $future->discardBuffers(); break; } } $log_stdout->finalize($start_stdout); $log_stderr->finalize($start_stderr); if ($err) { throw new HarbormasterBuildFailureException(); } } public function getArtifactInputs() { return array( array( 'name' => pht('Run on Host'), 'key' => $this->getSetting('hostartifact'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getFieldSpecifications() { return array( 'command' => array( 'name' => pht('Command'), 'type' => 'text', 'required' => true, 'caption' => pht( "Under Windows, this is executed under PowerShell. ". "Under UNIX, this is executed using the user's shell."), ), 'hostartifact' => array( 'name' => pht('Host'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php index 5d361b3de3..3910ac0b24 100644 --- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php @@ -1,109 +1,113 @@ getSetting('uri'); if ($uri) { $domain = id(new PhutilURI($uri))->getDomain(); } $method = $this->formatSettingForDescription('method', 'POST'); $domain = $this->formatValueForDescription($domain); if ($this->getSetting('credential')) { return pht( 'Make an authenticated HTTP %s request to %s.', $method, $domain); } else { return pht( 'Make an HTTP %s request to %s.', $method, $domain); } } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $viewer = PhabricatorUser::getOmnipotentUser(); $settings = $this->getSettings(); $variables = $build_target->getVariables(); $uri = $this->mergeVariables( 'vurisprintf', $settings['uri'], $variables); $log_body = $build->createLog($build_target, $uri, 'http-body'); $start = $log_body->start(); $method = nonempty(idx($settings, 'method'), 'POST'); $future = id(new HTTPSFuture($uri)) ->setMethod($method) ->setTimeout(60); $credential_phid = $this->getSetting('credential'); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, $viewer); $future->setHTTPBasicAuthCredentials( $key->getUsernameEnvelope()->openEnvelope(), $key->getPasswordEnvelope()); } list($status, $body, $headers) = $this->resolveFuture( $build, $build_target, $future); $log_body->append($body); $log_body->finalize($start); if ($status->getStatusCode() != 200) { $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); } } public function getFieldSpecifications() { return array( 'uri' => array( 'name' => pht('URI'), 'type' => 'text', 'required' => true, ), 'method' => array( 'name' => pht('HTTP Method'), 'type' => 'select', 'options' => array_fuse(array('POST', 'GET', 'PUT', 'DELETE')), ), 'credential' => array( 'name' => pht('Credentials'), 'type' => 'credential', 'credential.type' => PassphrasePasswordCredentialType::CREDENTIAL_TYPE, 'credential.provides' => PassphrasePasswordCredentialType::PROVIDES_TYPE, ), ); } public function supportsWaitForMessage() { return true; } } diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php index 28e14c964e..d5f461506a 100644 --- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php @@ -1,70 +1,74 @@ getSettings(); // Create the lease. $lease = id(new DrydockLease()) ->setResourceType('host') ->setAttributes( array( 'platform' => $settings['platform'], )) ->queueForActivation(); // Wait until the lease is fulfilled. // TODO: This will throw an exception if the lease can't be fulfilled; // we should treat that as build failure not build error. $lease->waitUntilActive(); // Create the associated artifact. $artifact = $build->createArtifact( $build_target, $settings['name'], HarbormasterBuildArtifact::TYPE_HOST); $artifact->setArtifactData(array( 'drydock-lease' => $lease->getID(), )); $artifact->save(); } public function getArtifactOutputs() { return array( array( 'name' => pht('Leased Host'), 'key' => $this->getSetting('name'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getFieldSpecifications() { return array( 'name' => array( 'name' => pht('Artifact Name'), 'type' => 'text', 'required' => true, ), 'platform' => array( 'name' => pht('Platform'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php index e008d5d50b..dc59717a23 100644 --- a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php @@ -1,83 +1,88 @@ formatSettingForDescription('artifact'), $this->formatSettingForDescription('path')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $path = $this->mergeVariables( 'vsprintf', $settings['path'], $variables); $artifact = $build->loadArtifact($settings['artifact']); $file = $artifact->loadPhabricatorFile(); $fragment = id(new PhragmentFragmentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPaths(array($path)) ->executeOne(); if ($fragment === null) { PhragmentFragment::createFromFile( PhabricatorUser::getOmnipotentUser(), $file, $path, PhabricatorPolicies::getMostOpenPolicy(), PhabricatorPolicies::POLICY_USER); } else { if ($file->getMimeType() === 'application/zip') { $fragment->updateFromZIP(PhabricatorUser::getOmnipotentUser(), $file); } else { $fragment->updateFromFile(PhabricatorUser::getOmnipotentUser(), $file); } } } public function getArtifactInputs() { return array( array( 'name' => pht('Publishes File'), 'key' => $this->getSetting('artifact'), 'type' => HarbormasterBuildArtifact::TYPE_FILE, ), ); } public function getFieldSpecifications() { return array( 'path' => array( 'name' => pht('Path'), 'type' => 'text', 'required' => true, ), 'artifact' => array( 'name' => pht('File Artifact'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php index 968c5b99cb..60221b782c 100644 --- a/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php @@ -1,59 +1,64 @@ formatSettingForDescription('seconds')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $target = time() + $settings['seconds']; // Use $build_update so that we only reload every 5 seconds, but // the sleep mechanism remains accurate. $build_update = 5; while (time() < $target) { sleep(1); if ($build_update <= 0) { $build->reload(); $build_update = 5; if ($this->shouldAbort($build, $build_target)) { throw new HarbormasterBuildAbortedException(); } } else { $build_update -= 1; } } } public function getFieldSpecifications() { return array( 'seconds' => array( 'name' => pht('Seconds'), 'type' => 'int', 'required' => true, 'caption' => pht('The number of seconds to sleep for.'), ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php b/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php index 402a03ba20..06947317ef 100644 --- a/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php +++ b/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php @@ -1,21 +1,25 @@ formatSettingForDescription('path'), $this->formatSettingForDescription('hostartifact')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $path = $this->mergeVariables( 'vsprintf', $settings['path'], $variables); $artifact = $build->loadArtifact($settings['hostartifact']); $lease = $artifact->loadDrydockLease(); $interface = $lease->getInterface('filesystem'); // TODO: Handle exceptions. $file = $interface->saveFile($path, $settings['name']); // Insert the artifact record. $artifact = $build->createArtifact( $build_target, $settings['name'], HarbormasterBuildArtifact::TYPE_FILE); $artifact->setArtifactData(array( 'filePHID' => $file->getPHID(), )); $artifact->save(); } public function getArtifactInputs() { return array( array( 'name' => pht('Upload From Host'), 'key' => $this->getSetting('hostartifact'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getArtifactOutputs() { return array( array( 'name' => pht('Uploaded File'), 'key' => $this->getSetting('name'), 'type' => HarbormasterBuildArtifact::TYPE_FILE, ), ); } public function getFieldSpecifications() { return array( 'path' => array( 'name' => pht('Path'), 'type' => 'text', 'required' => true, ), 'name' => array( 'name' => pht('Local Name'), 'type' => 'text', 'required' => true, ), 'hostartifact' => array( 'name' => pht('Host Artifact'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php index 288cb1b1fa..f4e965b6c2 100644 --- a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php @@ -1,111 +1,115 @@ getBuildable(); $object = $buildable->getBuildableObject(); if (!($object instanceof PhabricatorRepositoryCommit)) { return; } // Block until all previous builds of the same build plan have // finished. $plan = $build->getBuildPlan(); $existing_logs = id(new HarbormasterBuildLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); if ($existing_logs) { $log = head($existing_logs); } else { $log = $build->createLog($build_target, 'waiting', 'blockers'); } $blockers = $this->getBlockers($object, $plan, $build); if ($blockers) { $log->start(); $log->append(pht("Blocked by: %s\n", implode(',', $blockers))); $log->finalize(); } if ($blockers) { throw new PhabricatorWorkerYieldException(15); } } private function getBlockers( PhabricatorRepositoryCommit $commit, HarbormasterBuildPlan $plan, HarbormasterBuild $source) { $call = new ConduitCall( 'diffusion.commitparentsquery', array( 'commit' => $commit->getCommitIdentifier(), 'callsign' => $commit->getRepository()->getCallsign(), )); $call->setUser(PhabricatorUser::getOmnipotentUser()); $parents = $call->execute(); $parents = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepository($commit->getRepository()) ->withIdentifiers($parents) ->execute(); $blockers = array(); $build_objects = array(); foreach ($parents as $parent) { if (!$parent->isImported()) { $blockers[] = pht('Commit %s', $parent->getCommitIdentifier()); } else { $build_objects[] = $parent->getPHID(); } } if ($build_objects) { $buildables = id(new HarbormasterBuildableQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildablePHIDs($build_objects) ->withManualBuildables(false) ->execute(); $buildable_phids = mpull($buildables, 'getPHID'); if ($buildable_phids) { $builds = id(new HarbormasterBuildQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildablePHIDs($buildable_phids) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); foreach ($builds as $build) { if (!$build->isComplete()) { $blockers[] = pht('Build %d', $build->getID()); } } } } return $blockers; } } diff --git a/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php b/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php new file mode 100644 index 0000000000..bb439b75b4 --- /dev/null +++ b/src/applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php @@ -0,0 +1,52 @@ +getConstant('GROUPKEY'); + if ($const === false) { + throw new Exception( + pht( + '"%s" class "%s" must define a "%s" property.', + __CLASS__, + get_class($this), + 'GROUPKEY')); + } + + return $const; + } + + final public static function getAllGroups() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getGroupKey') + ->setSortMethod('getGroupOrder') + ->execute(); + } + + final public static function getAllEnabledGroups() { + $groups = self::getAllGroups(); + + foreach ($groups as $key => $group) { + if (!$group->isEnabled()) { + unset($groups[$key]); + } + } + + return $groups; + } + +} diff --git a/src/applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php b/src/applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php new file mode 100644 index 0000000000..c730048e78 --- /dev/null +++ b/src/applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php @@ -0,0 +1,20 @@ +setIsManualBuildable(0) ->setBuildableStatus(self::STATUS_BUILDING); } public function getMonogram() { return 'B'.$this->getID(); } /** * Returns an existing buildable for the object's PHID or creates a * new buildable implicitly if needed. */ public static function createOrLoadExisting( PhabricatorUser $actor, $buildable_object_phid, $container_object_phid) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($actor) ->withBuildablePHIDs(array($buildable_object_phid)) ->withManualBuildables(false) ->setLimit(1) ->executeOne(); if ($buildable) { return $buildable; } $buildable = self::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Looks up the plan PHIDs and applies the plans to the specified * object identified by it's PHID. */ public static function applyBuildPlans( $phid, $container_phid, array $plan_phids) { - if (count($plan_phids) === 0) { + if (!$plan_phids) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } $buildable = self::createOrLoadExisting( PhabricatorUser::getOmnipotentUser(), $phid, $container_phid); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($plan_phids) ->execute(); foreach ($plans as $plan) { if ($plan->isDisabled()) { // TODO: This should be communicated more clearly -- maybe we should // create the build but set the status to "disabled" or "derelict". continue; } $buildable->applyPlan($plan); } } public function applyPlan(HarbormasterBuildPlan $plan) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildStatus(HarbormasterBuild::STATUS_PENDING); $auto_key = $plan->getPlanAutoKey(); if ($auto_key) { $build->setPlanAutoKey($auto_key); } $build->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), )); return $build; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', 'isManualBuildable' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_container' => array( 'columns' => array('containerPHID'), ), 'key_manual' => array( 'columns' => array('isManualBuildable'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildablePHIDType::TYPECONST); } public function attachBuildableObject($buildable_object) { $this->buildableObject = $buildable_object; return $this; } public function getBuildableObject() { return $this->assertAttached($this->buildableObject); } public function attachContainerObject($container_object) { $this->containerObject = $container_object; return $this; } public function getContainerObject() { return $this->assertAttached($this->containerObject); } public function attachContainerHandle($container_handle) { $this->containerHandle = $container_handle; return $this; } public function getContainerHandle() { return $this->assertAttached($this->containerHandle); } public function attachBuildableHandle($buildable_handle) { $this->buildableHandle = $buildable_handle; return $this; } public function getBuildableHandle() { return $this->assertAttached($this->buildableHandle); } public function attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildableTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildableObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildableObject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A buildable inherits policies from the underlying object.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { // NOTE: This is essentially just for convenience, as it allows you create // a copy of a buildable by specifying `B123` without bothering to go // look up the underlying object. return $this->getBuildablePHID(); } public function getHarbormasterContainerPHID() { return $this->getContainerPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 439780e466..8d1128d009 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,460 +1,476 @@ setBuildStatus(self::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_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', 'planAutoKey' => 'text32?', ), 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, ), ), ) + 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->getBuildStatus() === self::STATUS_PENDING || $this->getBuildStatus() === self::STATUS_BUILDING; } + public function isAutobuild() { + return ($this->getPlanAutoKey() !== null); + } + public function createLog( HarbormasterBuildTarget $build_target, $log_source, $log_type) { $log_source = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(250) ->truncateString($log_source); $log = HarbormasterBuildLog::initializeNewBuildLog($build_target) ->setLogSource($log_source) ->setLogType($log_type) ->save(); return $log; } public function createArtifact( HarbormasterBuildTarget $build_target, $artifact_key, $artifact_type) { $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target); $artifact->setArtifactKey( $this->getPHID(), $this->getBuildGeneration(), $artifact_key); $artifact->setArtifactType($artifact_type); $artifact->save(); return $artifact; } public function loadArtifact($name) { $artifact = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withArtifactKeys( $this->getPHID(), $this->getBuildGeneration(), array($name)) ->executeOne(); if ($artifact === null) { throw new Exception(pht('Artifact not found!')); } return $artifact; } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, ); $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass('HarbormasterBuildableInterface') ->loadObjects(); $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.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { switch ($this->getBuildStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ERROR: case self::STATUS_STOPPED: return true; } return false; } public function isStopped() { return ($this->getBuildStatus() == self::STATUS_STOPPED); } /* -( 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()) { + return false; + } + return !$this->isRestarting(); } public function canStopBuild() { + if ($this->isAutobuild()) { + return false; + } + return !$this->isComplete() && !$this->isStopped() && !$this->isStopping(); } public function canResumeBuild() { + if ($this->isAutobuild()) { + return false; + } + return $this->isStopped() && !$this->isResuming(); } public function isStopping() { $is_stopping = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_STOP: $is_stopping = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_stopping = false; break; } } return $is_stopping; } 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_STOP: $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 deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); unset($this->unprocessedCommands[$key]); } return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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.'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php index c9957b0149..2bd37b2f79 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php @@ -1,120 +1,159 @@ setBuildTargetPHID($build_target->getPHID()); } + public static function getParameterSpec() { + return array( + 'name' => array( + 'type' => 'string', + 'description' => pht( + 'Short message name, like "Syntax Error".'), + ), + 'code' => array( + 'type' => 'string', + 'description' => pht( + 'Lint message code identifying the type of message, like "ERR123".'), + ), + 'severity' => array( + 'type' => 'string', + 'description' => pht( + 'Severity of the message.'), + ), + 'path' => array( + 'type' => 'string', + 'description' => pht( + 'Path to the file containing the lint message, from the project '. + 'root.'), + ), + 'line' => array( + 'type' => 'optional int', + 'description' => pht( + 'Line number in the file where the text which triggered the '. + 'message first appears. The first line of the file is line 1, '. + 'not line 0.'), + ), + 'char' => array( + 'type' => 'optional int', + 'description' => pht( + 'Byte position on the line where the text which triggered the '. + 'message starts. The first byte on the line is byte 1, not byte '. + '0. This position is byte-based (not character-based) because '. + 'not all lintable files have a valid character encoding.'), + ), + 'description' => array( + 'type' => 'optional string', + 'description' => pht( + 'Long explanation of the lint message.'), + ), + ); + } + public static function newFromDictionary( HarbormasterBuildTarget $build_target, array $dict) { $obj = self::initializeNewLintMessage($build_target); - $spec = array( - 'path' => 'string', - 'line' => 'optional int', - 'char' => 'optional int', - 'code' => 'string', - 'severity' => 'string', - 'name' => 'string', - 'description' => 'optional string', - ); + $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->setPath($dict['path']); $obj->setLine(idx($dict, 'line')); $obj->setCharacterOffset(idx($dict, 'char')); $obj->setCode($dict['code']); $obj->setSeverity($dict['severity']); $obj->setName($dict['name']); $description = idx($dict, 'description'); if (strlen($description)) { $obj->setProperty('description', $description); } return $obj; } protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'path' => 'text', 'line' => 'uint32?', 'characterOffset' => 'uint32?', 'code' => 'text32', 'severity' => 'text32', 'name' => 'text255', ), 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 getSortKey() { // TODO: Maybe use more numeric values after T6861. $map = array( ArcanistLintSeverity::SEVERITY_ERROR => 'A', ArcanistLintSeverity::SEVERITY_WARNING => 'B', ArcanistLintSeverity::SEVERITY_AUTOFIX => 'C', ArcanistLintSeverity::SEVERITY_ADVICE => 'Y', ArcanistLintSeverity::SEVERITY_DISABLED => 'Z', ); $severity = idx($map, $this->getSeverity(), 'N'); $parts = array( $severity, $this->getPath(), sprintf('%08d', $this->getLine()), $this->getCode(), ); return implode("\0", $parts); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php index 2b9108b035..1062b64a3a 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php @@ -1,122 +1,160 @@ 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', + 'description' => pht( + 'Coverage information for this test.'), + ), + ); + } + public static function newFromDictionary( HarbormasterBuildTarget $build_target, array $dict) { $obj = self::initializeNewUnitMessage($build_target); - $spec = array( - 'engine' => 'optional string', - 'namespace' => 'optional string', - 'name' => 'string', - 'result' => 'string', - 'duration' => 'optional float|int', - 'path' => 'optional string', - 'coverage' => 'optional map', - ); + $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); } 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', '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 getSortKey() { // TODO: Maybe use more numeric values after T6861. $map = array( ArcanistUnitTestResult::RESULT_FAIL => 'A', ArcanistUnitTestResult::RESULT_BROKEN => 'B', ArcanistUnitTestResult::RESULT_UNSOUND => 'C', ArcanistUnitTestResult::RESULT_PASS => 'Z', ); $result = idx($map, $this->getResult(), 'N'); $parts = array( $result, $this->getEngine(), $this->getNamespace(), $this->getName(), $this->getID(), ); return implode("\0", $parts); } } diff --git a/src/applications/help/controller/PhabricatorHelpEditorProtocolController.php b/src/applications/help/controller/PhabricatorHelpEditorProtocolController.php index 51dd4cfa0f..010e41ffcd 100644 --- a/src/applications/help/controller/PhabricatorHelpEditorProtocolController.php +++ b/src/applications/help/controller/PhabricatorHelpEditorProtocolController.php @@ -1,52 +1,51 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setMethod('GET') ->setSubmitURI('/settings/panel/display/') ->setTitle(pht('Unsupported Editor Protocol')) ->appendParagraph( pht( 'Your configured editor URI uses an unsupported protocol. Change '. 'your settings to use a supported protocol, or ask your '. 'administrator to add support for the chosen protocol by '. 'configuring: %s', phutil_tag('tt', array(), 'uri.allowed-editor-protocols'))) ->addSubmitButton(pht('Change Settings')) ->addCancelButton('/'); return id(new AphrontDialogResponse()) ->setDialog($dialog); } public static function hasAllowedProtocol($uri) { $uri = new PhutilURI($uri); $editor_protocol = $uri->getProtocol(); if (!$editor_protocol) { // The URI must have a protocol. return false; } $allowed_key = 'uri.allowed-editor-protocols'; $allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key); if (empty($allowed_protocols[$editor_protocol])) { // The protocol must be on the allowed protocol whitelist. return false; } return true; } } diff --git a/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php b/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php index f91182859a..b136265d7f 100644 --- a/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php +++ b/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php @@ -1,71 +1,70 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $keys = $request->getStr('keys'); try { $keys = phutil_json_decode($keys); } catch (PhutilJSONParserException $ex) { return new Aphront400Response(); } // There have been at least two users asking for a keyboard shortcut to // close the dialog, so be explicit that escape works since it isn't // terribly discoverable. $keys[] = array( 'keys' => array('esc'), 'description' => pht('Close any dialog, including this one.'), ); $stroke_map = array( 'left' => "\xE2\x86\x90", 'right' => "\xE2\x86\x92", 'up' => "\xE2\x86\x91", 'down' => "\xE2\x86\x93", 'return' => "\xE2\x8F\x8E", 'tab' => "\xE2\x87\xA5", 'delete' => "\xE2\x8C\xAB", ); $rows = array(); foreach ($keys as $shortcut) { $keystrokes = array(); foreach ($shortcut['keys'] as $stroke) { $stroke = idx($stroke_map, $stroke, $stroke); $keystrokes[] = phutil_tag('kbd', array(), $stroke); } $keystrokes = phutil_implode_html(' or ', $keystrokes); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('th', array(), $keystrokes), phutil_tag('td', array(), $shortcut['description']), )); } $table = phutil_tag( 'table', array('class' => 'keyboard-shortcut-help'), $rows); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Keyboard Shortcuts')) ->appendChild($table) ->addCancelButton('#', pht('Close')); return id(new AphrontDialogResponse()) ->setDialog($dialog); } } diff --git a/src/applications/herald/action/HeraldAction.php b/src/applications/herald/action/HeraldAction.php new file mode 100644 index 0000000000..02a9dfa60a --- /dev/null +++ b/src/applications/herald/action/HeraldAction.php @@ -0,0 +1,407 @@ +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_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: + $handles = $viewer->loadHandles($target); + $handles = iterator_to_array($handles); + return mpull($handles, 'getName', 'getPHID'); + } + + 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() { + $class = new ReflectionClass($this); + + $const = $class->getConstant('ACTIONCONST'); + if ($const === false) { + throw new Exception( + pht( + '"%s" class "%s" must define a "%s" property.', + __CLASS__, + get_class($this), + 'ACTIONCONST')); + } + + $limit = self::getActionConstantByteLimit(); + if (!is_string($const) || (strlen($const) > $limit)) { + throw new Exception( + pht( + '"%s" class "%s" has an invalid "%s" property. Action constants '. + 'must be strings and no more than %s bytes in length.', + __CLASS__, + get_class($this), + 'ACTIONCONST', + new PhutilNumber($limit))); + } + + return $const; + } + + final public static function getActionConstantByteLimit() { + return 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'), + ), + ); + } + + 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: + return pht( + 'This action has no effect on %s target(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_STANDARD_INVALID: + return pht( + '%s target(s) are invalid or of the wrong type: %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_STANDARD_UNLOADABLE: + return pht( + '%s target(s) could not be loaded: %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_STANDARD_PERMISSION: + return pht( + '%s target(s) do not have permission to see this object: %s.', + new PhutilNumber(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); + } + + return null; + } + +} diff --git a/src/applications/herald/field/HeraldFieldGroup.php b/src/applications/herald/action/HeraldActionGroup.php similarity index 53% copy from src/applications/herald/field/HeraldFieldGroup.php copy to src/applications/herald/action/HeraldActionGroup.php index a8f2f229d5..a087909609 100644 --- a/src/applications/herald/field/HeraldFieldGroup.php +++ b/src/applications/herald/action/HeraldActionGroup.php @@ -1,38 +1,28 @@ getConstant('FIELDGROUPKEY'); + $const = $class->getConstant('ACTIONGROUPKEY'); if ($const === false) { throw new Exception( pht( '"%s" class "%s" must define a "%s" property.', __CLASS__, get_class($this), - 'FIELDCONST')); + 'ACTIONGROUPKEY')); } return $const; } - public function getSortKey() { - return sprintf('A%08d%s', $this->getGroupOrder(), $this->getGroupLabel()); - } - - final public static function getAllFieldGroups() { + final public static function getAllActionGroups() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getGroupKey') ->setSortMethod('getSortKey') ->execute(); } } diff --git a/src/applications/herald/action/HeraldApplicationActionGroup.php b/src/applications/herald/action/HeraldApplicationActionGroup.php new file mode 100644 index 0000000000..b2b6e8d2c6 --- /dev/null +++ b/src/applications/herald/action/HeraldApplicationActionGroup.php @@ -0,0 +1,15 @@ +logEffect(self::DO_NOTHING); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + protected function getActionEffectMap() { + return array( + self::DO_NOTHING => array( + 'icon' => 'fa-check', + 'color' => 'grey', + 'name' => pht('Did Nothing'), + ), + ); + } + + public function renderActionDescription($value) { + return pht('Do nothing.'); + } + + protected function renderActionEffectDescription($type, $data) { + return pht('Did nothing.'); + } + +} diff --git a/src/applications/herald/action/HeraldNotifyActionGroup.php b/src/applications/herald/action/HeraldNotifyActionGroup.php new file mode 100644 index 0000000000..df8c395341 --- /dev/null +++ b/src/applications/herald/action/HeraldNotifyActionGroup.php @@ -0,0 +1,15 @@ +emailPHIDs); } public function getForcedEmailPHIDs() { return array_values($this->forcedEmailPHIDs); } - public function getCustomActions() { - if ($this->customActions === null) { - $custom_actions = id(new PhutilSymbolLoader()) - ->setAncestorClass('HeraldCustomAction') - ->loadObjects(); - - foreach ($custom_actions as $key => $object) { - if (!$object->appliesToAdapter($this)) { - unset($custom_actions[$key]); - } - } - - $this->customActions = array(); - foreach ($custom_actions as $action) { - $key = $action->getActionKey(); - - if (array_key_exists($key, $this->customActions)) { - throw new Exception( - pht( - "More than one Herald custom action implementation ". - "handles the action key: '%s'.", - $key)); - } - - $this->customActions[$key] = $action; - } + public function addEmailPHID($phid, $force) { + $this->emailPHIDs[$phid] = $phid; + if ($force) { + $this->forcedEmailPHIDs[$phid] = $phid; } - - return $this->customActions; + return $this; } 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; } - protected function handleCustomHeraldEffect(HeraldEffect $effect) { - $custom_action = idx($this->getCustomActions(), $effect->getAction()); - - if ($custom_action !== null) { - return $custom_action->applyEffect( - $this, - $this->getObject(), - $effect); - } - - return null; - } - public function isAvailableToUser(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array($this->getAdapterApplicationClass())) ->execute(); return !empty($applications); } public function queueTransaction($transaction) { $this->queuedTransactions[] = $transaction; } public function getQueuedTransactions() { return $this->queuedTransactions; } - protected function newTransaction() { + 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(); } /** * 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 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_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_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: // "Contains" can take an array of strings, as in "Any changed // filename" for diffs. foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return true; } } return false; case self::CONDITION_NOT_CONTAINS: return (stripos($field_value, $condition_value) === false); 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: 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 is not valid!')); } if ($result) { return true; } } return false; 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: $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_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 )------------------------------------------------------------ */ - public function getCustomActionsForRuleType($rule_type) { - $results = array(); - foreach ($this->getCustomActions() as $custom_action) { - if ($custom_action->appliesToRuleType($rule_type)) { - $results[] = $custom_action; + 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 $results; + + return $this->actionMap; } - public function getActions($rule_type) { - $custom_actions = $this->getCustomActionsForRuleType($rule_type); - $custom_actions = mpull($custom_actions, 'getActionKey'); + 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))); + } - $actions = $custom_actions; + return $action; + } - $object = $this->newObject(); + private function getActionsForRuleType($rule_type) { + $actions = $this->getActionImplementationMap(); - if (($object instanceof PhabricatorProjectInterface)) { - if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) { - $actions[] = self::ACTION_ADD_PROJECTS; - $actions[] = self::ACTION_REMOVE_PROJECTS; + foreach ($actions as $key => $action) { + if (!$action->supportsRuleType($rule_type)) { + unset($actions[$key]); } } return $actions; } - public function getActionNameMap($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: - $standard = array( - self::ACTION_NOTHING => pht('Do nothing'), - self::ACTION_ADD_CC => pht('Add Subscribers'), - self::ACTION_REMOVE_CC => pht('Remove Subscribers'), - self::ACTION_EMAIL => pht('Send an email to'), - self::ACTION_AUDIT => pht('Trigger an Audit by'), - self::ACTION_FLAG => pht('Mark with flag'), - self::ACTION_ASSIGN_TASK => pht('Assign task to'), - self::ACTION_ADD_PROJECTS => pht('Add projects'), - self::ACTION_REMOVE_PROJECTS => pht('Remove projects'), - self::ACTION_ADD_REVIEWERS => pht('Add reviewers'), - self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'), - self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'), - self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'), - self::ACTION_BLOCK => pht('Block change with message'), - ); - break; - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - $standard = array( - self::ACTION_NOTHING => pht('Do nothing'), - self::ACTION_ADD_CC => pht('Add me as a subscriber'), - self::ACTION_REMOVE_CC => pht('Remove me as a subscriber'), - self::ACTION_EMAIL => pht('Send me an email'), - self::ACTION_AUDIT => pht('Trigger an Audit by me'), - self::ACTION_FLAG => pht('Mark with flag'), - self::ACTION_ASSIGN_TASK => pht('Assign task to me'), - self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'), - self::ACTION_ADD_BLOCKING_REVIEWERS => - pht('Add me as a blocking reviewer'), - ); - break; - default: - throw new Exception(pht("Unknown rule type '%s'!", $rule_type)); + 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; } - $custom_actions = $this->getCustomActionsForRuleType($rule_type); - $standard += mpull($custom_actions, 'getActionName', 'getActionKey'); + return $actions; + } + + public function getActionNameMap($rule_type) { + $map = array(); + foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { + $map[$key] = $action->getHeraldActionName(); + } - return $standard; + return $map; } public function willSaveAction( HeraldRule $rule, HeraldActionRecord $action) { + $impl = $this->requireActionImplementation($action->getAction()); $target = $action->getTarget(); - if (is_array($target)) { - $target = array_keys($target); - } - - $author_phid = $rule->getAuthorPHID(); - - $rule_type = $rule->getRuleType(); - if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { - switch ($action->getAction()) { - case self::ACTION_EMAIL: - case self::ACTION_ADD_CC: - case self::ACTION_REMOVE_CC: - case self::ACTION_AUDIT: - case self::ACTION_ASSIGN_TASK: - case self::ACTION_ADD_REVIEWERS: - case self::ACTION_ADD_BLOCKING_REVIEWERS: - // For personal rules, force these actions to target the rule owner. - $target = array($author_phid); - break; - case self::ACTION_FLAG: - // Make sure flag color is valid; set to blue if not. - $color_map = PhabricatorFlagColor::getColorNameMap(); - if (empty($color_map[$target])) { - $target = PhabricatorFlagColor::COLOR_BLUE; - } - break; - case self::ACTION_BLOCK: - case self::ACTION_NOTHING: - break; - default: - throw new HeraldInvalidActionException( - pht( - 'Unrecognized action type "%s"!', - $action->getAction())); - } - } + $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) { - $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); - - if ($is_personal) { - switch ($action) { - case self::ACTION_ADD_CC: - case self::ACTION_REMOVE_CC: - case self::ACTION_EMAIL: - case self::ACTION_NOTHING: - case self::ACTION_AUDIT: - case self::ACTION_ASSIGN_TASK: - case self::ACTION_ADD_REVIEWERS: - case self::ACTION_ADD_BLOCKING_REVIEWERS: - return new HeraldEmptyFieldValue(); - case self::ACTION_FLAG: - return $this->buildFlagColorFieldValue(); - case self::ACTION_ADD_PROJECTS: - case self::ACTION_REMOVE_PROJECTS: - return $this->buildTokenizerFieldValue( - new PhabricatorProjectDatasource()); - } - } else { - switch ($action) { - case self::ACTION_ADD_CC: - case self::ACTION_REMOVE_CC: - case self::ACTION_EMAIL: - return $this->buildTokenizerFieldValue( - new PhabricatorMetaMTAMailableDatasource()); - case self::ACTION_NOTHING: - return new HeraldEmptyFieldValue(); - case self::ACTION_ADD_PROJECTS: - case self::ACTION_REMOVE_PROJECTS: - return $this->buildTokenizerFieldValue( - new PhabricatorProjectDatasource()); - case self::ACTION_FLAG: - return $this->buildFlagColorFieldValue(); - case self::ACTION_ASSIGN_TASK: - return $this->buildTokenizerFieldValue( - new PhabricatorPeopleDatasource()); - case self::ACTION_AUDIT: - case self::ACTION_ADD_REVIEWERS: - case self::ACTION_ADD_BLOCKING_REVIEWERS: - return $this->buildTokenizerFieldValue( - new PhabricatorProjectOrUserDatasource()); - case self::ACTION_APPLY_BUILD_PLANS: - return $this->buildTokenizerFieldValue( - new HarbormasterBuildPlanDatasource()); - case self::ACTION_REQUIRE_SIGNATURE: - return $this->buildTokenizerFieldValue( - new LegalpadDocumentDatasource()); - case self::ACTION_BLOCK: - return new HeraldTextFieldValue(); - } - } - - $custom_action = idx($this->getCustomActions(), $action); - if ($custom_action !== null) { - return $custom_action->getActionType(); - } - - throw new Exception(pht("Unknown or invalid action '%s'.", $action)); - } - - private function buildFlagColorFieldValue() { - return id(new HeraldSelectFieldValue()) - ->setKey('flag.color') - ->setOptions(PhabricatorFlagColor::getColorNameMap()) - ->setDefault(PhabricatorFlagColor::COLOR_BLUE); + $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() { return array( HeraldRepetitionPolicyConfig::EVERY, ); } abstract protected function initializeNewAdapter(); /** * 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, - array $handles) { + 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()) ->setIconFont('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), )); } $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt( HeraldRepetitionPolicyConfig::EVERY); if ($rule->getRepetitionPolicy() == $integer_code_for_every) { $action_text = pht('Take these actions every time this rule matches:'); } else { $action_text = pht('Take these actions the first 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($action, $handles), + $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); - $default = pht('(Unknown Field "%s")', $field_type); + if (!$field) { + return pht('Unknown Field: "%s"', $field_type); + } - $field_name = idx($this->getFieldNameMap(), $field_type, $default); + $field_name = $field->getHeraldFieldName(); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition, $handles, $viewer); - return hsprintf(' %s %s %s', $field_name, $condition_name, $value); + 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()) { - case self::ACTION_FLAG: - $target[$index] = PhabricatorFlagColor::getColorName($val); - break; 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(); - $supported = $this->getActions($rule_type); - $supported = array_fuse($supported); - if (empty($supported[$action])) { - return new HeraldApplyTranscript( - $effect, - false, - pht( - 'Adapter "%s" does not support action "%s" for rule type "%s".', - get_class($this), - $action, - $rule_type)); - } - - switch ($action) { - case self::ACTION_ADD_PROJECTS: - case self::ACTION_REMOVE_PROJECTS: - return $this->applyProjectsEffect($effect); - case self::ACTION_ADD_CC: - case self::ACTION_REMOVE_CC: - return $this->applySubscribersEffect($effect); - case self::ACTION_FLAG: - return $this->applyFlagEffect($effect); - case self::ACTION_EMAIL: - return $this->applyEmailEffect($effect); - case self::ACTION_NOTHING: - return $this->applyNothingEffect($effect); - default: - break; - } - $result = $this->handleCustomHeraldEffect($effect); - - if (!$result) { + $impl = $this->getActionImplementation($action); + if (!$impl) { return new HeraldApplyTranscript( $effect, false, - pht( - 'No custom action exists to handle rule action "%s".', - $action)); - } - - return $result; - } - - private function applyNothingEffect(HeraldEffect $effect) { - return new HeraldApplyTranscript( - $effect, - true, - pht('Did nothing.')); - } - - /** - * @task apply - */ - private function applyProjectsEffect(HeraldEffect $effect) { - - if ($effect->getAction() == self::ACTION_ADD_PROJECTS) { - $kind = '+'; - } else { - $kind = '-'; - } - - $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; - $project_phids = $effect->getTarget(); - $xaction = $this->newTransaction() - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $project_type) - ->setNewValue( array( - $kind => array_fuse($project_phids), + array( + HeraldAction::DO_STANDARD_INVALID_ACTION, + $action, + ), )); - - $this->queueTransaction($xaction); - - return new HeraldApplyTranscript( - $effect, - true, - pht('Added projects.')); - } - - /** - * @task apply - */ - private function applySubscribersEffect(HeraldEffect $effect) { - if ($effect->getAction() == self::ACTION_ADD_CC) { - $kind = '+'; - $is_add = true; - } else { - $kind = '-'; - $is_add = false; } - $subscriber_phids = array_fuse($effect->getTarget()); - if (!$subscriber_phids) { + if (!$impl->supportsRuleType($rule_type)) { return new HeraldApplyTranscript( $effect, false, - pht('This action lists no users or objects to affect.')); - } - - // The "Add Subscribers" rule only adds subscribers who haven't previously - // unsubscribed from the object explicitly. Filter these subscribers out - // before continuing. - $unsubscribed = array(); - if ($is_add) { - if ($this->unsubscribedPHIDs === null) { - $this->unsubscribedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getObject()->getPHID(), - PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST); - } - - foreach ($this->unsubscribedPHIDs as $phid) { - if (isset($subscriber_phids[$phid])) { - $unsubscribed[$phid] = $phid; - unset($subscriber_phids[$phid]); - } - } - } - - if (!$subscriber_phids) { - return new HeraldApplyTranscript( - $effect, - false, - pht('All targets have previously unsubscribed explicitly.')); - } - - // Filter out PHIDs which aren't valid subscribers. Lower levels of the - // stack will fail loudly if we try to add subscribers with invalid PHIDs - // or unknown PHID types, so drop them here. - $invalid = array(); - foreach ($subscriber_phids as $phid) { - $type = phid_get_type($phid); - switch ($type) { - case PhabricatorPeopleUserPHIDType::TYPECONST: - case PhabricatorProjectProjectPHIDType::TYPECONST: - break; - default: - $invalid[$phid] = $phid; - unset($subscriber_phids[$phid]); - break; - } - } - - if (!$subscriber_phids) { - return new HeraldApplyTranscript( - $effect, - false, - pht('All targets are invalid as subscribers.')); - } - - $xaction = $this->newTransaction() - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) - ->setNewValue( array( - $kind => $subscriber_phids, + array( + HeraldAction::DO_STANDARD_WRONG_RULE_TYPE, + $rule_type, + ), )); - - $this->queueTransaction($xaction); - - // TODO: We could be more detailed about this, but doing it meaningfully - // probably requires substantial changes to how transactions are rendered - // first. - if ($is_add) { - $message = pht('Subscribed targets.'); - } else { - $message = pht('Unsubscribed targets.'); } - return new HeraldApplyTranscript($effect, true, $message); + $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); - /** - * @task apply - */ - private function applyFlagEffect(HeraldEffect $effect) { - $phid = $this->getPHID(); - $color = $effect->getTarget(); - - $rule = $effect->getRule(); - $user = $rule->getAuthor(); - - $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); - if ($flag) { - return new HeraldApplyTranscript( - $effect, - false, - pht('Object already flagged.')); + $this->edgeCache[$type] = array_fuse($phids); } - - $handle = id(new PhabricatorHandleQuery()) - ->setViewer($user) - ->withPHIDs(array($phid)) - ->executeOne(); - - $flag = new PhabricatorFlag(); - $flag->setOwnerPHID($user->getPHID()); - $flag->setType($handle->getType()); - $flag->setObjectPHID($handle->getPHID()); - - // TOOD: Should really be transcript PHID, but it doesn't exist yet. - $flag->setReasonPHID($user->getPHID()); - - $flag->setColor($color); - $flag->setNote( - pht('Flagged by Herald Rule "%s".', $rule->getName())); - $flag->save(); - - return new HeraldApplyTranscript( - $effect, - true, - pht('Added flag.')); + return $this->edgeCache[$type]; } - - /** - * @task apply - */ - private function applyEmailEffect(HeraldEffect $effect) { - foreach ($effect->getTarget() as $phid) { - $this->emailPHIDs[$phid] = $phid; - - // If this is a personal rule, we'll force delivery of a real email. This - // effect is stronger than notification preferences, so you get an actual - // email even if your preferences are set to "Notify" or "Ignore". - $rule = $effect->getRule(); - if ($rule->isPersonalRule()) { - $this->forcedEmailPHIDs[$phid] = $phid; - } - } - - return new HeraldApplyTranscript( - $effect, - true, - pht('Added mailable to mail targets.')); - } - - } diff --git a/src/applications/herald/application/PhabricatorHeraldApplication.php b/src/applications/herald/application/PhabricatorHeraldApplication.php index b74a27d9f2..e3537d9722 100644 --- a/src/applications/herald/application/PhabricatorHeraldApplication.php +++ b/src/applications/herald/application/PhabricatorHeraldApplication.php @@ -1,77 +1,77 @@ pht('Herald User Guide'), 'href' => PhabricatorEnv::getDoclink('Herald User Guide'), ), ); } public function getFlavorText() { return pht('Watch for danger!'); } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function getRemarkupRules() { return array( new HeraldRemarkupRule(), ); } public function getRoutes() { return array( '/herald/' => array( '(?:query/(?P[^/]+)/)?' => 'HeraldRuleListController', 'new/' => 'HeraldNewController', 'rule/(?P[1-9]\d*)/' => 'HeraldRuleViewController', 'edit/(?:(?P[1-9]\d*)/)?' => 'HeraldRuleController', 'disable/(?P[1-9]\d*)/(?P\w+)/' => 'HeraldDisableController', 'test/' => 'HeraldTestConsoleController', 'transcript/' => array( '' => 'HeraldTranscriptListController', '(?:query/(?P[^/]+)/)?' => 'HeraldTranscriptListController', - '(?P[1-9]\d*)/(?:(?P\w+)/)?' + '(?P[1-9]\d*)/' => 'HeraldTranscriptController', ), ), ); } protected function getCustomCapabilities() { return array( HeraldManageGlobalRulesCapability::CAPABILITY => array( 'caption' => pht('Global rules can bypass access controls.'), 'default' => PhabricatorPolicies::POLICY_ADMIN, ), ); } } diff --git a/src/applications/herald/controller/HeraldDisableController.php b/src/applications/herald/controller/HeraldDisableController.php index edf15f80f6..054f30e7d3 100644 --- a/src/applications/herald/controller/HeraldDisableController.php +++ b/src/applications/herald/controller/HeraldDisableController.php @@ -1,74 +1,66 @@ id = $data['id']; - $this->action = $data['action']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - $id = $this->id; + 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 = $this->getApplicationURI("rule/{$id}/"); - $is_disable = ($this->action === 'disable'); + $is_disable = ($action === 'disable'); if ($request->isFormPost()) { $xaction = id(new HeraldRuleTransaction()) ->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE) ->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 archive this rule?'); $body = pht('This rule will no longer activate.'); $button = pht('Archive Rule'); } else { $title = pht('Really activate this rule?'); $body = pht('This rule will become active again.'); $button = pht('Activate 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 2bbc00456f..53e1faf7d6 100644 --- a/src/applications/herald/controller/HeraldNewController.php +++ b/src/applications/herald/controller/HeraldNewController.php @@ -1,317 +1,316 @@ getRequest(); - $viewer = $request->getUser(); + 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, )); $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(); break; case 1: $rule_types = $this->renderRuleTypeControl( $rule_type_map, $e_rule); $form ->addHiddenInput('content_type', $content_type) ->addHiddenInput('step', 2) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Rule for')) ->setValue( phutil_tag( 'strong', array(), idx($content_type_map, $content_type)))) ->appendChild($rule_types); $cancel_text = pht('Back'); $cancel_uri = id(new PhutilURI('new/')) ->setQueryParams( array( 'content_type' => $content_type, 'step' => 0, )); $cancel_uri = $this->getApplicationURI($cancel_uri); 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'))); $cancel_text = pht('Back'); $cancel_uri = id(new PhutilURI('new/')) ->setQueryParams( array( 'content_type' => $content_type, 'rule_type' => $rule_type, 'step' => 1, )); $cancel_uri = $this->getApplicationURI($cancel_uri); break; } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue')) ->addCancelButton($cancel_uri, $cancel_text)); $form_box = id(new PHUIObjectBoxView()) ->setFormErrors($errors) ->setHeaderText(pht('Create Herald Rule')) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Create Rule')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create Herald Rule'), )); } 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 5f447abffd..ce7a46cd00 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -1,670 +1,668 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = (int)idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); + $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); - if ($this->id) { - $id = $this->id; + if ($id) { $rule = id(new HeraldRuleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI("rule/{$id}/"); } else { $rule = new HeraldRule(); - $rule->setAuthorPHID($user->getPHID()); + $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])) { $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; } $rule->setRuleType($rule_type); $adapter = HeraldAdapter::getAdapterForContentType( $rule->getContentType()); if (!$adapter->supportsRuleType($rule->getRuleType())) { throw new Exception( pht( "This rule's content type does not support the selected rule ". "type.")); } if ($rule->isObjectRule()) { $rule->setTriggerObjectPHID($request->getStr('targetPHID')); $object = id(new PhabricatorObjectQuery()) - ->setViewer($user) + ->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 = $this->getApplicationURI("rule/{$id}/"); 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($user) + ->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 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 green', 'sigil' => 'create-action', 'mustcapture' => true, ), pht('New Action'))) ->setDescription(pht( 'Take these actions %s this rule matches:', $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') : pht('Create Herald Rule'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Edit Rule'), )); } private function saveRule(HeraldAdapter $adapter, $rule, $request) { $rule->setName($request->getStr('name')); $match_all = ($request->getStr('must_match') == 'all'); $rule->setMustMatchAll((int)$match_all); $repetition_policy_param = $request->getStr('repetition_policy'); $rule->setRepetitionPolicy( HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)); $e_name = true; $errors = array(); if (!strlen($rule->getName())) { $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; } $rule->attachConditions($conditions); $rule->attachActions($actions); if (!$errors) { $edit_action = $rule->getID() ? 'edit' : 'create'; $rule->openTransaction(); $rule->save(); $rule->saveConditions($conditions); $rule->saveActions($actions); $rule->saveTransaction(); } return array($e_name, $errors); } private function setupEditorBehavior( HeraldRule $rule, array $handles, HeraldAdapter $adapter) { $serial_conditions = array( array('default', 'default', ''), ); if ($rule->getConditions()) { $serial_conditions = array(); foreach ($rule->getConditions() as $condition) { $value = $adapter->getEditorValueForCondition( $this->getViewer(), - $condition, - $handles); + $condition); $serial_conditions[] = array( $condition->getFieldName(), $condition->getFieldCondition(), $value, ); } } $serial_actions = array( array('default', ''), ); if ($rule->getActions()) { $serial_actions = array(); foreach ($rule->getActions() as $action) { - switch ($action->getAction()) { - case HeraldAdapter::ACTION_FLAG: - case HeraldAdapter::ACTION_BLOCK: - $current_value = $action->getTarget(); - break; - default: - if (is_array($action->getTarget())) { - $target_map = array(); - foreach ((array)$action->getTarget() as $fbid) { - $target_map[$fbid] = $handles[$fbid]->getName(); - } - $current_value = $target_map; - } else { - $current_value = $action->getTarget(); - } - break; - } + $value = $adapter->getEditorValueForAction( + $this->getViewer(), + $action); $serial_actions[] = array( $action->getAction(), - $current_value, + $value, ); } } $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('', $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('', $action_name); } } - $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; - } - - $field_groups = HeraldFieldGroup::getAllFieldGroups(); - - $groups = array(); - foreach ($group_map as $group_key => $options) { - asort($options); - - $field_group = idx($field_groups, $group_key); - if ($field_group) { - $group_label = $field_group->getGroupLabel(); - $group_order = $field_group->getSortKey(); - } else { - $group_label = nonempty($group_key, pht('Other')); - $group_order = 'Z'; - } - - $groups[] = array( - 'label' => $group_label, - 'options' => $options, - 'order' => $group_order, - ); - } - - $groups = array_values(isort($groups, 'order')); - $config_info = array(); - $config_info['fields'] = $groups; + $config_info['fields'] = $this->getFieldGroups($adapter, $field_map); $config_info['conditions'] = $all_conditions; - $config_info['actions'] = $action_map; + $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 ($config_info['actions'] as $action => $name) { + 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; } Javelin::initBehavior( 'herald-rule-editor', array( 'root' => 'herald-rule-edit-form', '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 = HeraldRepetitionPolicyConfig::toString( $rule->getRepetitionPolicy()); $repetition_options = $adapter->getRepetitionOptions(); $repetition_names = HeraldRepetitionPolicyConfig::getMap(); $repetition_map = array_select_keys($repetition_names, $repetition_options); if (count($repetition_map) < 2) { return head($repetition_names); } else { return AphrontFormSelectControl::renderSelectTag( $repetition_policy, $repetition_map, array( 'name' => 'repetition_policy', )); } } 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/HeraldRuleListController.php b/src/applications/herald/controller/HeraldRuleListController.php index 8847b5d06b..490d84212d 100644 --- a/src/applications/herald/controller/HeraldRuleListController.php +++ b/src/applications/herald/controller/HeraldRuleListController.php @@ -1,25 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new HeraldRuleSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index f1ba8e44d7..f6ec235a9e 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -1,162 +1,156 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $rule = id(new HeraldRuleQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needConditionsAndActions(true) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($rule->getName()) ->setPolicyObject($rule); if ($rule->getIsDisabled()) { $header->setStatus( 'fa-ban', 'red', pht('Archived')); } else { $header->setStatus( 'fa-check', 'bluegrey', pht('Active')); } $actions = $this->buildActionView($rule); $properties = $this->buildPropertyView($rule, $actions); $id = $rule->getID(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb("H{$id}"); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $timeline = $this->buildTransactionTimeline( $rule, new HeraldTransactionQuery()); $timeline->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, ), array( 'title' => $rule->getName(), )); } private function buildActionView(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $id = $rule->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($rule) ->setObjectURI($this->getApplicationURI("rule/{$id}/")); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $rule, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Rule')) ->setHref($this->getApplicationURI("edit/{$id}/")) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($rule->getIsDisabled()) { $disable_uri = "disable/{$id}/enable/"; $disable_icon = 'fa-check'; $disable_name = pht('Activate Rule'); } else { $disable_uri = "disable/{$id}/disable/"; $disable_icon = 'fa-ban'; $disable_name = pht('Archive Rule'); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Rule')) ->setHref($this->getApplicationURI($disable_uri)) ->setIcon($disable_icon) ->setName($disable_name) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( HeraldRule $rule, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($rule) ->setActionList($actions); $view->addProperty( pht('Rule Type'), idx(HeraldRuleTypeConfig::getRuleTypeMap(), $rule->getRuleType())); if ($rule->isPersonalRule()) { $view->addProperty( pht('Author'), $viewer->renderHandle($rule->getAuthorPHID())); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { $view->addProperty( pht('Applies To'), idx( HeraldAdapter::getEnabledAdapterMap($viewer), $rule->getContentType())); if ($rule->isObjectRule()) { $view->addProperty( pht('Trigger Object'), $viewer->renderHandle($rule->getTriggerObjectPHID())); } $view->invokeWillRenderEvent(); $view->addSectionHeader( pht('Rule Description'), PHUIPropertyListView::ICON_SUMMARY); $handles = $viewer->loadHandles(HeraldAdapter::getHandlePHIDs($rule)); $rule_text = $adapter->renderRuleAsText($rule, $handles, $viewer); $view->addTextContent($rule_text); } return $view; } } diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 7cd610f35a..a7741ba3ce 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -1,119 +1,114 @@ getRequest(); - $user = $request->getUser(); - - $request = $this->getRequest(); - + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $object_name = trim($request->getStr('object_name')); $e_name = true; $errors = array(); if ($request->isFormPost()) { if (!$object_name) { $e_name = pht('Required'); $errors[] = pht('An object name is required.'); } if (!$errors) { $object = id(new PhabricatorObjectQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withNames(array($object_name)) ->executeOne(); if (!$object) { $e_name = pht('Invalid'); $errors[] = pht('No object exists with that name.'); } if (!$errors) { // TODO: Let the adapters claim objects instead. if ($object instanceof DifferentialRevision) { $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $object, $object->loadActiveDiff()); } else if ($object instanceof PhabricatorRepositoryCommit) { $adapter = id(new HeraldCommitAdapter()) ->setCommit($object); } else if ($object instanceof ManiphestTask) { $adapter = id(new HeraldManiphestTaskAdapter()) ->setTask($object); } else if ($object instanceof PholioMock) { $adapter = id(new HeraldPholioMockAdapter()) ->setMock($object); } else if ($object instanceof PhrictionDocument) { $adapter = id(new PhrictionDocumentHeraldAdapter()) ->setDocument($object); } else { throw new Exception(pht('Can not build adapter for object!')); } $adapter->setIsNewObject(false); $rules = id(new HeraldRuleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withContentTypes(array($adapter->getAdapterContentType())) ->withDisabled(false) ->needConditionsAndActions(true) ->needAppliedToPHIDs(array($object->getPHID())) ->needValidateAuthors(true) ->execute(); $engine = id(new HeraldEngine()) ->setDryRun(true); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); return id(new AphrontRedirectResponse()) ->setURI('/herald/transcript/'.$xscript->getID().'/'); } } } $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendRemarkupInstructions( pht( 'Enter an object to test rules for, like a Diffusion commit (e.g., '. '`rX123`) or a Differential revision (e.g., `D123`). You will be '. 'shown the results of a dry run on the object.')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Object Name')) ->setName('object_name') ->setError($e_name) ->setValue($object_name)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Test Rules'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Herald Test Console')) ->setFormErrors($errors) ->setForm($form); $nav = $this->buildSideNavView(); $nav->selectFilter('test'); $nav->appendChild($box); $crumbs = id($this->buildApplicationCrumbs()) ->addTextCrumb(pht('Test Console')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Test Console'), )); } } diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php index eac808b54e..64563b7ef8 100644 --- a/src/applications/herald/controller/HeraldTranscriptController.php +++ b/src/applications/herald/controller/HeraldTranscriptController.php @@ -1,559 +1,467 @@ id = $data['id']; - $map = $this->getFilterMap(); - $this->filter = idx($data, 'filter'); - if (empty($map[$this->filter])) { - $this->filter = self::FILTER_ALL; - } - } - private function getAdapter() { return $this->adapter; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $xscript = id(new HeraldTranscriptQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$xscript) { return new Aphront404Response(); } require_celerity_resource('herald-test-css'); - - $nav = $this->buildSideNav(); + $content = array(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { $notice = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Old Transcript')) ->appendChild(phutil_tag( 'p', array(), pht('Details of this transcript have been garbage collected.'))); - $nav->appendChild($notice); + $content[] = $notice; } else { $map = HeraldAdapter::getEnabledAdapterMap($viewer); $object_type = $object_xscript->getType(); if (empty($map[$object_type])) { // TODO: We should filter these out in the Query, but we have to load // the objectTranscript right now, which is potentially enormous. We // should denormalize the object type, or move the data into a separate // table, and then filter this earlier (and thus raise a better error). // For now, just block access so we don't violate policies. throw new Exception( pht('This transcript has an invalid or inaccessible adapter.')); } $this->adapter = HeraldAdapter::getAdapterForContentType($object_type); - $filter = $this->getFilterPHIDs(); - $this->filterTranscript($xscript, $filter); - $phids = array_merge($filter, $this->getTranscriptPHIDs($xscript)); + $phids = $this->getTranscriptPHIDs($xscript); $phids = array_unique($phids); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $this->handles = $handles; if ($xscript->getDryRun()) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('Dry Run')); $notice->appendChild( pht( 'This was a dry run to test Herald rules, '. 'no actions were executed.')); - $nav->appendChild($notice); + $content[] = $notice; } $warning_panel = $this->buildWarningPanel($xscript); - $nav->appendChild($warning_panel); - - $apply_xscript_panel = $this->buildApplyTranscriptPanel( - $xscript); - $nav->appendChild($apply_xscript_panel); - - $action_xscript_panel = $this->buildActionTranscriptPanel( - $xscript); - $nav->appendChild($action_xscript_panel); + $content[] = $warning_panel; - $object_xscript_panel = $this->buildObjectTranscriptPanel( - $xscript); - $nav->appendChild($object_xscript_panel); + $content[] = array( + $this->buildActionTranscriptPanel($xscript), + $this->buildObjectTranscriptPanel($xscript), + ); } $crumbs = id($this->buildApplicationCrumbs()) ->addTextCrumb( pht('Transcripts'), $this->getApplicationURI('/transcript/')) ->addTextCrumb($xscript->getID()); - $nav->setCrumbs($crumbs); return $this->buildApplicationPage( - $nav, + array( + $crumbs, + $content, + ), array( 'title' => pht('Transcript'), )); } protected function renderConditionTestValue($condition, $handles) { // TODO: This is all a hacky mess and should be driven through FieldValue // eventually. switch ($condition->getFieldName()) { case HeraldAnotherRuleField::FIELDCONST: $value = array($condition->getTestValue()); break; default: $value = $condition->getTestValue(); break; } if (!is_scalar($value) && $value !== null) { foreach ($value as $key => $phid) { $handle = idx($handles, $phid); if ($handle && $handle->isComplete()) { $value[$key] = $handle->getName(); } else { // This happens for things like task priorities, statuses, and // custom fields. $value[$key] = $phid; } } sort($value); $value = implode(', ', $value); } return phutil_tag('span', array('class' => 'condition-test-value'), $value); } - private function buildSideNav() { - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI('/herald/transcript/'.$this->id.'/')); - - $items = array(); - $filters = $this->getFilterMap(); - foreach ($filters as $key => $name) { - $nav->addFilter($key, $name); - } - $nav->selectFilter($this->filter, null); - - return $nav; - } - - protected function getFilterMap() { - return array( - self::FILTER_ALL => pht('All Rules'), - self::FILTER_OWNED => pht('Rules I Own'), - self::FILTER_AFFECTED => pht('Rules that Affected Me'), - ); - } - - - protected function getFilterPHIDs() { - return array($this->getRequest()->getUser()->getPHID()); - } - protected function getTranscriptPHIDs($xscript) { $phids = array(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { return array(); } $phids[] = $object_xscript->getPHID(); foreach ($xscript->getApplyTranscripts() as $apply_xscript) { // TODO: This is total hacks. Add another amazing layer of abstraction. $target = (array)$apply_xscript->getTarget(); foreach ($target as $phid) { if ($phid) { $phids[] = $phid; } } } foreach ($xscript->getRuleTranscripts() as $rule_xscript) { $phids[] = $rule_xscript->getRuleOwner(); } $condition_xscripts = $xscript->getConditionTranscripts(); if ($condition_xscripts) { $condition_xscripts = call_user_func_array( 'array_merge', $condition_xscripts); } foreach ($condition_xscripts as $condition_xscript) { switch ($condition_xscript->getFieldName()) { case HeraldAnotherRuleField::FIELDCONST: $phids[] = $condition_xscript->getTestValue(); break; default: $value = $condition_xscript->getTestValue(); // TODO: Also total hacks. if (is_array($value)) { foreach ($value as $phid) { if ($phid) { // TODO: Probably need to make sure this // "looks like" a PHID or decrease the level of hacks here; // this used to be an is_numeric() check in Facebook land. $phids[] = $phid; } } } break; } } return $phids; } - protected function filterTranscript($xscript, $filter_phids) { - $filter_owned = ($this->filter == self::FILTER_OWNED); - $filter_affected = ($this->filter == self::FILTER_AFFECTED); - - if (!$filter_owned && !$filter_affected) { - // No filtering to be done. - return; - } - - if (!$xscript->getObjectTranscript()) { - return; - } - - $user_phid = $this->getRequest()->getUser()->getPHID(); - - $keep_apply_xscripts = array(); - $keep_rule_xscripts = array(); - - $filter_phids = array_fill_keys($filter_phids, true); - - $rule_xscripts = $xscript->getRuleTranscripts(); - foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) { - $rule_id = $apply_xscript->getRuleID(); - if ($filter_owned) { - if (empty($rule_xscripts[$rule_id])) { - // No associated rule so you can't own this effect. - continue; - } - if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) { - continue; - } - } else if ($filter_affected) { - $targets = (array)$apply_xscript->getTarget(); - if (!array_select_keys($filter_phids, $targets)) { - continue; - } - } - $keep_apply_xscripts[$id] = true; - if ($rule_id) { - $keep_rule_xscripts[$rule_id] = true; - } - } - - foreach ($rule_xscripts as $rule_id => $rule_xscript) { - if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) { - $keep_rule_xscripts[$rule_id] = true; - } - } - - $xscript->setRuleTranscripts( - array_intersect_key( - $xscript->getRuleTranscripts(), - $keep_rule_xscripts)); - - $xscript->setApplyTranscripts( - array_intersect_key( - $xscript->getApplyTranscripts(), - $keep_apply_xscripts)); - - $xscript->setConditionTranscripts( - array_intersect_key( - $xscript->getConditionTranscripts(), - $keep_rule_xscripts)); - } - private function buildWarningPanel(HeraldTranscript $xscript) { $request = $this->getRequest(); $panel = null; if ($xscript->getObjectTranscript()) { $handles = $this->handles; $object_xscript = $xscript->getObjectTranscript(); $handle = $handles[$object_xscript->getPHID()]; if ($handle->getType() == PhabricatorRepositoryCommitPHIDType::TYPECONST) { $commit = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withPHIDs(array($handle->getPHID())) ->executeOne(); if ($commit) { $repository = $commit->getRepository(); if ($repository->isImporting()) { $title = pht( 'The %s repository is still importing.', $repository->getMonogram()); $body = pht( 'Herald rules will not trigger until import completes.'); } else if (!$repository->isTracked()) { $title = pht( 'The %s repository is not tracked.', $repository->getMonogram()); $body = pht( 'Herald rules will not trigger until tracking is enabled.'); } else { return $panel; } $panel = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle($title) ->appendChild($body); } } } return $panel; } - private function buildApplyTranscriptPanel(HeraldTranscript $xscript) { - $handles = $this->handles; + private function buildActionTranscriptPanel(HeraldTranscript $xscript) { + $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); + $adapter = $this->getAdapter(); - $rule_type_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; - $action_names = $adapter->getActionNameMap($rule_type_global); + $field_names = $adapter->getFieldNameMap(); + $condition_names = $adapter->getConditionNameMap(); - $list = new PHUIObjectItemListView(); - $list->setStates(true); - $list->setNoDataString(pht('No actions were taken.')); - foreach ($xscript->getApplyTranscripts() as $apply_xscript) { + $handles = $this->handles; - $target = $apply_xscript->getTarget(); - switch ($apply_xscript->getAction()) { - case HeraldAdapter::ACTION_NOTHING: - $target = null; - break; - case HeraldAdapter::ACTION_FLAG: - $target = PhabricatorFlagColor::getColorName($target); - break; - case HeraldAdapter::ACTION_BLOCK: - // Target is a text string. - $target = $target; - break; - default: - if (is_array($target) && $target) { - foreach ($target as $k => $phid) { - if (isset($handles[$phid])) { - $target[$k] = $handles[$phid]->getName(); - } - } - $target = implode(', ', $target); - } else if (is_string($target)) { - $target = $target; - } else { - $target = ''; - } - break; - } + $action_map = $xscript->getApplyTranscripts(); + $action_map = mgroup($action_map, 'getRuleID'); - $item = new PHUIObjectItemView(); + $rule_list = id(new PHUIObjectItemListView()) + ->setNoDataString(pht('No Herald rules applied to this object.')); - if ($apply_xscript->getApplied()) { - $item->setState(PHUIObjectItemView::STATE_SUCCESS); - } else { - $item->setState(PHUIObjectItemView::STATE_FAIL); + $rule_xscripts = $xscript->getRuleTranscripts(); + $rule_xscripts = msort($rule_xscripts, 'getRuleID'); + foreach ($rule_xscripts as $rule_xscript) { + $rule_id = $rule_xscript->getRuleID(); + + $rule_item = id(new PHUIObjectItemView()) + ->setObjectName(pht('H%d', $rule_id)) + ->setHeader($rule_xscript->getRuleName()); + + if (!$rule_xscript->getResult()) { + $rule_item->setDisabled(true); } - $rule = idx( - $action_names, - $apply_xscript->getAction(), - pht('Unknown Action "%s"', $apply_xscript->getAction())); + $rule_list->addItem($rule_item); - $item->setHeader(pht('%s: %s', $rule, $target)); - $item->addAttribute($apply_xscript->getReason()); - $item->addAttribute( - pht('Outcome: %s', $apply_xscript->getAppliedReason())); + // Build the field/condition transcript. - $list->addItem($item); - } + $cond_xscripts = $xscript->getConditionTranscriptsForRule($rule_id); - $box = new PHUIObjectBoxView(); - $box->setHeaderText(pht('Actions Taken')); - $box->appendChild($list); + $cond_list = id(new PHUIStatusListView()); + $cond_list->addItem( + id(new PHUIStatusItemView()) + ->setTarget(phutil_tag('strong', array(), pht('Conditions')))); - return $box; - } + foreach ($cond_xscripts as $cond_xscript) { + if ($cond_xscript->getResult()) { + $icon = 'fa-check'; + $color = 'green'; + $result = pht('Passed'); + } else { + $icon = 'fa-times'; + $color = 'red'; + $result = pht('Failed'); + } - private function buildActionTranscriptPanel(HeraldTranscript $xscript) { - $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); + if ($cond_xscript->getNote()) { + $note = phutil_tag( + 'div', + array( + 'class' => 'herald-condition-note', + ), + $cond_xscript->getNote()); + } else { + $note = null; + } - $adapter = $this->getAdapter(); + // TODO: This is not really translatable and should be driven through + // HeraldField. + $explanation = pht( + '%s %s %s', + idx($field_names, $cond_xscript->getFieldName(), pht('Unknown')), + idx($condition_names, $cond_xscript->getCondition(), pht('Unknown')), + $this->renderConditionTestValue($cond_xscript, $handles)); + $cond_item = id(new PHUIStatusItemView()) + ->setIcon($icon, $color) + ->setTarget($result) + ->setNote(array($explanation, $note)); - $field_names = $adapter->getFieldNameMap(); - $condition_names = $adapter->getConditionNameMap(); + $cond_list->addItem($cond_item); + } - $handles = $this->handles; + if ($rule_xscript->getResult()) { + $last_icon = 'fa-check-circle'; + $last_color = 'green'; + $last_result = pht('Passed'); + $last_note = pht('Rule passed.'); + } else { + $last_icon = 'fa-times-circle'; + $last_color = 'red'; + $last_result = pht('Failed'); + $last_note = pht('Rule failed.'); + } + + $cond_last = id(new PHUIStatusItemView()) + ->setIcon($last_icon, $last_color) + ->setTarget(phutil_tag('strong', array(), $last_result)) + ->setNote($last_note); + $cond_list->addItem($cond_last); + + $cond_box = id(new PHUIBoxView()) + ->appendChild($cond_list) + ->addMargin(PHUI::MARGIN_LARGE_LEFT); + + $rule_item->appendChild($cond_box); + + if (!$rule_xscript->getResult()) { + // If the rule didn't pass, don't generate an action transcript since + // actions didn't apply. + continue; + } + + $cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM); - $rule_markup = array(); - foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) { - $cond_markup = array(); - foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) { - if ($cond->getNote()) { - $note = phutil_tag_div('herald-condition-note', $cond->getNote()); + $action_xscripts = idx($action_map, $rule_id, array()); + foreach ($action_xscripts as $action_xscript) { + $action_key = $action_xscript->getAction(); + $action = $adapter->getActionImplementation($action_key); + + if ($action) { + $name = $action->getHeraldActionName(); + $action->setViewer($this->getViewer()); } else { - $note = null; + $name = pht('Unknown Action ("%s")', $action_key); } - if ($cond->getResult()) { - $result = phutil_tag( - 'span', - array('class' => 'herald-outcome condition-pass'), - "\xE2\x9C\x93"); - } else { - $result = phutil_tag( - 'span', - array('class' => 'herald-outcome condition-fail'), - "\xE2\x9C\x98"); + $name = pht('Action: %s', $name); + + $action_list = id(new PHUIStatusListView()); + $action_list->addItem( + id(new PHUIStatusItemView()) + ->setTarget(phutil_tag('strong', array(), $name))); + + $action_box = id(new PHUIBoxView()) + ->appendChild($action_list) + ->addMargin(PHUI::MARGIN_LARGE_LEFT); + + $rule_item->appendChild($action_box); + + $log = $action_xscript->getAppliedReason(); + + // Handle older transcripts which used a static string to record + // action results. + if (!is_array($log)) { + $action_list->addItem( + id(new PHUIStatusItemView()) + ->setIcon('fa-clock-o', 'grey') + ->setTarget(pht('Old Transcript')) + ->setNote( + pht( + 'This is an old transcript which uses an obsolete log '. + 'format. Detailed action information is not available.'))); + continue; } - $cond_markup[] = phutil_tag( - 'li', - array(), - pht( - '%s Condition: %s %s %s%s', - $result, - idx($field_names, $cond->getFieldName(), pht('Unknown')), - idx($condition_names, $cond->getCondition(), pht('Unknown')), - $this->renderConditionTestValue($cond, $handles), - $note)); - } + foreach ($log as $entry) { + $type = idx($entry, 'type'); + $data = idx($entry, 'data'); - if ($rule->getResult()) { - $result = phutil_tag( - 'span', - array('class' => 'herald-outcome rule-pass'), - pht('PASS')); - $class = 'herald-rule-pass'; - } else { - $result = phutil_tag( - 'span', - array('class' => 'herald-outcome rule-fail'), - pht('FAIL')); - $class = 'herald-rule-fail'; - } + if ($action) { + $icon = $action->renderActionEffectIcon($type, $data); + $color = $action->renderActionEffectColor($type, $data); + $name = $action->renderActionEffectName($type, $data); + $note = $action->renderEffectDescription($type, $data); + } else { + $icon = 'fa-question-circle'; + $color = 'indigo'; + $name = pht('Unknown Effect ("%s")', $type); + $note = null; + } - $cond_markup[] = phutil_tag( - 'li', - array(), - array($result, $rule->getReason())); - $user_phid = $this->getRequest()->getUser()->getPHID(); - - $name = $rule->getRuleName(); - - $rule_markup[] = - phutil_tag( - 'li', - array( - 'class' => $class, - ), - phutil_tag_div('rule-name', array( - phutil_tag('strong', array(), $name), - ' ', - phutil_tag('ul', array(), $cond_markup), - ))); - } + $action_item = id(new PHUIStatusItemView()) + ->setIcon($icon, $color) + ->setTarget($name) + ->setNote($note); - $box = null; - if ($rule_markup) { - $box = new PHUIObjectBoxView(); - $box->setHeaderText(pht('Rule Details')); - $box->appendChild(phutil_tag( - 'ul', - array('class' => 'herald-explain-list'), - $rule_markup)); + $action_list->addItem($action_item); + } + } } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Rule Transcript')) + ->appendChild($rule_list); + return $box; } private function buildObjectTranscriptPanel(HeraldTranscript $xscript) { $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $object_xscript = $xscript->getObjectTranscript(); $data = array(); if ($object_xscript) { $phid = $object_xscript->getPHID(); $handles = $this->handles; $data += array( pht('Object Name') => $object_xscript->getName(), pht('Object Type') => $object_xscript->getType(), pht('Object PHID') => $phid, pht('Object Link') => $handles[$phid]->renderLink(), ); } $data += $xscript->getMetadataMap(); if ($object_xscript) { foreach ($object_xscript->getFields() as $field => $value) { $field = idx($field_names, $field, '['.$field.'?]'); $data['Field: '.$field] = $value; } } $rows = array(); foreach ($data as $name => $value) { if (!($value instanceof PhutilSafeHTML)) { if (!is_scalar($value) && !is_null($value)) { $value = implode("\n", $value); } if (strlen($value) > 256) { $value = phutil_tag( 'textarea', array( 'class' => 'herald-field-value-transcript', ), $value); } } $rows[] = array($name, $value); } $property_list = new PHUIPropertyListView(); $property_list->setStacked(true); foreach ($rows as $row) { $property_list->addProperty($row[0], $row[1]); } $box = new PHUIObjectBoxView(); $box->setHeaderText(pht('Object Transcript')); $box->appendChild($property_list); return $box; } } diff --git a/src/applications/herald/controller/HeraldTranscriptListController.php b/src/applications/herald/controller/HeraldTranscriptListController.php index d53130d476..81865eee4a 100644 --- a/src/applications/herald/controller/HeraldTranscriptListController.php +++ b/src/applications/herald/controller/HeraldTranscriptListController.php @@ -1,48 +1,44 @@ getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); if ($for_app) { $nav->addFilter('new', pht('Create Rule')); } id(new HeraldTranscriptSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Transcripts'), $this->getApplicationURI('transcript/')); return $crumbs; } - public function willProcessRequest(array $data) { - $this->queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new HeraldTranscriptSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/herald/extension/HeraldCustomAction.php b/src/applications/herald/extension/HeraldCustomAction.php deleted file mode 100644 index 2701a8ae66..0000000000 --- a/src/applications/herald/extension/HeraldCustomAction.php +++ /dev/null @@ -1,20 +0,0 @@ -getConstant('FIELDGROUPKEY'); if ($const === false) { throw new Exception( pht( '"%s" class "%s" must define a "%s" property.', __CLASS__, get_class($this), - 'FIELDCONST')); + 'FIELDGROUPKEY')); } return $const; } - public function getSortKey() { - return sprintf('A%08d%s', $this->getGroupOrder(), $this->getGroupLabel()); - } - final public static function getAllFieldGroups() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getGroupKey') ->setSortMethod('getSortKey') ->execute(); } } diff --git a/src/applications/herald/group/HeraldGroup.php b/src/applications/herald/group/HeraldGroup.php new file mode 100644 index 0000000000..348ea1d24a --- /dev/null +++ b/src/applications/herald/group/HeraldGroup.php @@ -0,0 +1,15 @@ +getGroupOrder(), $this->getGroupLabel()); + } + +} diff --git a/src/applications/herald/query/HeraldRuleSearchEngine.php b/src/applications/herald/query/HeraldRuleSearchEngine.php index 63e6efc818..04b7a5d852 100644 --- a/src/applications/herald/query/HeraldRuleSearchEngine.php +++ b/src/applications/herald/query/HeraldRuleSearchEngine.php @@ -1,220 +1,214 @@ setParameter( 'authorPHIDs', $this->readUsersFromRequest($request, 'authors')); $saved->setParameter('contentType', $request->getStr('contentType')); $saved->setParameter('ruleType', $request->getStr('ruleType')); $saved->setParameter( 'disabled', $this->readBoolFromRequest($request, 'disabled')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new HeraldRuleQuery()); $author_phids = $saved->getParameter('authorPHIDs'); if ($author_phids) { $query->withAuthorPHIDs($author_phids); } $content_type = $saved->getParameter('contentType'); $content_type = idx($this->getContentTypeValues(), $content_type); if ($content_type) { $query->withContentTypes(array($content_type)); } $rule_type = $saved->getParameter('ruleType'); $rule_type = idx($this->getRuleTypeValues(), $rule_type); if ($rule_type) { $query->withRuleTypes(array($rule_type)); } $disabled = $saved->getParameter('disabled'); if ($disabled !== null) { $query->withDisabled($disabled); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $author_phids = $saved_query->getParameter('authorPHIDs', array()); $content_type = $saved_query->getParameter('contentType'); $rule_type = $saved_query->getParameter('ruleType'); $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('authors') ->setLabel(pht('Authors')) ->setValue($author_phids)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('contentType') ->setLabel(pht('Content Type')) ->setValue($content_type) ->setOptions($this->getContentTypeOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('ruleType') ->setLabel(pht('Rule Type')) ->setValue($rule_type) ->setOptions($this->getRuleTypeOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('disabled') ->setLabel(pht('Rule Status')) ->setValue($this->getBoolFromQuery($saved_query, 'disabled')) ->setOptions( array( '' => pht('Show Enabled and Disabled Rules'), 'false' => pht('Show Only Enabled Rules'), 'true' => pht('Show Only Disabled Rules'), ))); } 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('disabled', false); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer_phid)) ->setParameter('disabled', false); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getContentTypeOptions() { return array( '' => pht('(All Content Types)'), ) + HeraldAdapter::getEnabledAdapterMap($this->requireViewer()); } private function getContentTypeValues() { return array_fuse( array_keys( HeraldAdapter::getEnabledAdapterMap($this->requireViewer()))); } private function getRuleTypeOptions() { return array( '' => pht('(All Rule Types)'), ) + HeraldRuleTypeConfig::getRuleTypeMap(); } private function getRuleTypeValues() { return array_fuse(array_keys(HeraldRuleTypeConfig::getRuleTypeMap())); } protected function getRequiredHandlePHIDsForResultList( array $rules, PhabricatorSavedQuery $query) { return mpull($rules, 'getAuthorPHID'); } protected function renderResultList( array $rules, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($rules, 'HeraldRule'); $viewer = $this->requireViewer(); $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($rules as $rule) { $id = $rule->getID(); $item = id(new PHUIObjectItemView()) ->setObjectName("H{$id}") ->setHeader($rule->getName()) ->setHref($this->getApplicationURI("rule/{$id}/")); 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')); } - $item->addAction( - id(new PHUIListItemView()) - ->setHref($this->getApplicationURI("history/{$id}/")) - ->setIcon('fa-file-text-o') - ->setName(pht('Edit Log'))); - $content_type_name = idx($content_type_map, $rule->getContentType()); $item->addAttribute(pht('Affects: %s', $content_type_name)); $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No rules found.')); return $result; } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentCommentController.php b/src/applications/legalpad/controller/LegalpadDocumentCommentController.php index f0f557d67f..530696437b 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentCommentController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentCommentController.php @@ -1,78 +1,72 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $document = id(new LegalpadDocumentQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needDocumentBodies(true) ->executeOne(); if (!$document) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $document_uri = $this->getApplicationURI('view/'.$document->getID()); $comment = $request->getStr('comment'); $xactions = array(); if (strlen($comment)) { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new LegalpadTransactionComment()) ->setDocumentID($document->getID()) ->setLineNumber(0) ->setLineLength(0) ->setContent($comment)); } $editor = id(new LegalpadDocumentEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect($request->isContinueRequest()) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($document, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($document_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse())->setURI($document_uri); } } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentDoneController.php b/src/applications/legalpad/controller/LegalpadDocumentDoneController.php index e6601ba072..b12e3f65ad 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentDoneController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentDoneController.php @@ -1,22 +1,21 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); return $this->newDialog() ->setTitle(pht('Verify Signature')) ->appendParagraph( pht( 'Thank you for signing this document. Please check your email '. 'to verify your signature and complete the process.')) ->addCancelButton('/', pht('Okay')); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentEditController.php b/src/applications/legalpad/controller/LegalpadDocumentEditController.php index 140a26a670..edbaaf99e2 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentEditController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentEditController.php @@ -1,262 +1,262 @@ getUser(); - + $viewer = $request->getViewer(); $id = $request->getURIData('id'); + if (!$id) { $is_create = true; $this->requireApplicationCapability( LegalpadCreateDocumentsCapability::CAPABILITY); - $document = LegalpadDocument::initializeNewDocument($user); + $document = LegalpadDocument::initializeNewDocument($viewer); $body = id(new LegalpadDocumentBody()) - ->setCreatorPHID($user->getPHID()); + ->setCreatorPHID($viewer->getPHID()); $document->attachDocumentBody($body); $document->setDocumentBodyPHID(PhabricatorPHIDConstants::PHID_VOID); } else { $is_create = false; $document = id(new LegalpadDocumentQuery()) - ->setViewer($user) + ->setViewer($viewer) ->needDocumentBodies(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($id)) ->executeOne(); if (!$document) { return new Aphront404Response(); } } $e_title = true; $e_text = true; $title = $document->getDocumentBody()->getTitle(); $text = $document->getDocumentBody()->getText(); $v_signature_type = $document->getSignatureType(); $v_preamble = $document->getPreamble(); $v_require_signature = $document->getRequireSignature(); $errors = array(); $can_view = null; $can_edit = null; if ($request->isFormPost()) { $xactions = array(); $title = $request->getStr('title'); if (!strlen($title)) { $e_title = pht('Required'); $errors[] = pht('The document title may not be blank.'); } else { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_TITLE) ->setNewValue($title); } $text = $request->getStr('text'); if (!strlen($text)) { $e_text = pht('Required'); $errors[] = pht('The document may not be blank.'); } else { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_TEXT) ->setNewValue($text); } $can_view = $request->getStr('can_view'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($can_view); $can_edit = $request->getStr('can_edit'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($can_edit); if ($is_create) { $v_signature_type = $request->getStr('signatureType'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_SIGNATURE_TYPE) ->setNewValue($v_signature_type); } $v_preamble = $request->getStr('preamble'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_PREAMBLE) ->setNewValue($v_preamble); $v_require_signature = $request->getBool('requireSignature', 0); if ($v_require_signature) { - if (!$user->getIsAdmin()) { + if (!$viewer->getIsAdmin()) { $errors[] = pht('Only admins may require signature.'); } $individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; if ($v_signature_type != $individual) { $errors[] = pht( 'Only documents with signature type "individual" may require '. 'signing to use Phabricator.'); } } - if ($user->getIsAdmin()) { + if ($viewer->getIsAdmin()) { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_REQUIRE_SIGNATURE) ->setNewValue($v_require_signature); } if (!$errors) { $editor = id(new LegalpadDocumentEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) - ->setActor($user); + ->setActor($viewer); $xactions = $editor->applyTransactions($document, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('view/'.$document->getID())); } } if ($errors) { // set these to what was specified in the form on post $document->setViewPolicy($can_view); $document->setEditPolicy($can_edit); } $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setID('document-title') ->setLabel(pht('Title')) ->setError($e_title) ->setValue($title) ->setName('title')); if ($is_create) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Who Should Sign?')) ->setName(pht('signatureType')) ->setValue($v_signature_type) ->setOptions(LegalpadDocument::getSignatureTypeMap())); $show_require = true; $caption = pht('Applies only to documents individuals sign.'); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Who Should Sign?')) ->setValue($document->getSignatureTypeName())); $individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; $show_require = $document->getSignatureType() == $individual; $caption = null; } if ($show_require) { $form ->appendChild( id(new AphrontFormCheckboxControl()) - ->setDisabled(!$user->getIsAdmin()) + ->setDisabled(!$viewer->getIsAdmin()) ->setLabel(pht('Require Signature')) ->addCheckbox( 'requireSignature', 'requireSignature', pht('Should signing this document be required to use Phabricator?'), $v_require_signature) ->setCaption($caption)); } $form ->appendChild( id(new PhabricatorRemarkupControl()) - ->setUser($user) + ->setUser($viewer) ->setID('preamble') ->setLabel(pht('Preamble')) ->setValue($v_preamble) ->setName('preamble') ->setCaption( pht('Optional help text for users signing this document.'))) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setUser($user) + ->setUser($viewer) ->setID('document-text') ->setLabel(pht('Document Body')) ->setError($e_text) ->setValue($text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('text')); $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($document) ->execute(); $form ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_edit')); $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $submit = new AphrontFormSubmitControl(); if ($is_create) { $submit->setValue(pht('Create Document')); $submit->addCancelButton($this->getApplicationURI()); $title = pht('Create Document'); $short = pht('Create'); } else { $submit->setValue(pht('Save Document')); $submit->addCancelButton( $this->getApplicationURI('view/'.$document->getID())); $title = pht('Edit Document'); $short = pht('Edit'); $crumbs->addTextCrumb( $document->getMonogram(), $this->getApplicationURI('view/'.$document->getID())); } $form ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs->addTextCrumb($short); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) ->setPreviewURI($this->getApplicationURI('document/preview/')) ->setControlID('document-text') ->addClass('phui-document-view'); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => $title, )); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentListController.php b/src/applications/legalpad/controller/LegalpadDocumentListController.php index 3229107658..cbd92f87a9 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentListController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentListController.php @@ -1,42 +1,38 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new LegalpadDocumentSearchEngine()) ->setNavigation($this->buildSideNav()); return $this->delegateToController($controller); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $can_create = $this->hasApplicationCapability( LegalpadCreateDocumentsCapability::CAPABILITY); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Document')) ->setHref($this->getApplicationURI('create/')) ->setIcon('fa-plus-square') ->setDisabled(!$can_create) ->setWorkflow(!$can_create)); return $crumbs; } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentManageController.php b/src/applications/legalpad/controller/LegalpadDocumentManageController.php index f24c0d03ce..9fc773258f 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentManageController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentManageController.php @@ -1,207 +1,201 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); // NOTE: We require CAN_EDIT to view this page. $document = id(new LegalpadDocumentQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needDocumentBodies(true) ->needContributors(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$document) { return new Aphront404Response(); } $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); $document_body = $document->getDocumentBody(); $engine = id(new PhabricatorMarkupEngine()) - ->setViewer($user); + ->setViewer($viewer); $engine->addObject( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $timeline = $this->buildTransactionTimeline( $document, new LegalpadTransactionQuery(), $engine); $title = $document_body->getTitle(); $header = id(new PHUIHeaderView()) ->setHeader($title) - ->setUser($user) + ->setUser($viewer) ->setPolicyObject($document); $actions = $this->buildActionView($document); $properties = $this->buildPropertyView($document, $engine, $actions); $comment_form_id = celerity_generate_unique_node_id(); $add_comment = $this->buildAddCommentView($document, $comment_form_id); $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $crumbs->addTextCrumb( $document->getMonogram(), '/'.$document->getMonogram()); $crumbs->addTextCrumb(pht('Manage')); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties) ->addPropertyList($this->buildDocument($engine, $document_body)); $content = array( $crumbs, $object_box, $timeline, $add_comment, ); return $this->buildApplicationPage( $content, array( 'title' => $title, 'pageObjects' => array($document->getPHID()), )); } private function buildDocument( PhabricatorMarkupEngine $engine, LegalpadDocumentBody $body) { $view = new PHUIPropertyListView(); $view->addClass('legalpad'); $view->addSectionHeader(pht('Document')); $view->addTextContent( $engine->getOutput($body, LegalpadDocumentBody::MARKUP_FIELD_TEXT)); return $view; } private function buildActionView(LegalpadDocument $document) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $actions = id(new PhabricatorActionListView()) - ->setUser($user) + ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($document); $can_edit = PhabricatorPolicyFilter::hasCapability( - $user, + $viewer, $document, PhabricatorPolicyCapability::CAN_EDIT); $doc_id = $document->getID(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil-square') ->setName(pht('View/Sign Document')) ->setHref('/'.$document->getMonogram())); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Document')) ->setHref($this->getApplicationURI('/edit/'.$doc_id.'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-terminal') ->setName(pht('View Signatures')) ->setHref($this->getApplicationURI('/signatures/'.$doc_id.'/'))); return $actions; } private function buildPropertyView( LegalpadDocument $document, PhabricatorMarkupEngine $engine, PhabricatorActionListView $actions) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($user) + ->setUser($viewer) ->setObject($document) ->setActionList($actions); $properties->addProperty( pht('Signature Type'), $document->getSignatureTypeName()); $properties->addProperty( pht('Last Updated'), - phabricator_datetime($document->getDateModified(), $user)); + phabricator_datetime($document->getDateModified(), $viewer)); $properties->addProperty( pht('Updated By'), - $user->renderHandle($document->getDocumentBody()->getCreatorPHID())); + $viewer->renderHandle($document->getDocumentBody()->getCreatorPHID())); $properties->addProperty( pht('Versions'), $document->getVersions()); if ($document->getContributors()) { $properties->addProperty( pht('Contributors'), - $user + $viewer ->renderHandleList($document->getContributors()) ->setAsInline(true)); } $properties->invokeWillRenderEvent(); return $properties; } private function buildAddCommentView( LegalpadDocument $document, $comment_form_id) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); - $draft = PhabricatorDraft::newFromUserAndKey($user, $document->getPHID()); + $draft = PhabricatorDraft::newFromUserAndKey($viewer, $document->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('Debate Legislation'); $form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setObjectPHID($document->getPHID()) ->setFormID($comment_form_id) ->setHeaderText($title) ->setDraft($draft) ->setSubmitButtonName(pht('Add Comment')) ->setAction($this->getApplicationURI('/comment/'.$document->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php index 945ad2dc39..5330049aae 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureListController.php @@ -1,86 +1,80 @@ documentID = idx($data, 'id'); - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $querykey = $request->getURIData('queryKey'); - if ($this->documentID) { + if ($id) { $document = id(new LegalpadDocumentQuery()) - ->setViewer($user) - ->withIDs(array($this->documentID)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$document) { return new Aphront404Response(); } $this->document = $document; } $engine = id(new LegalpadDocumentSignatureSearchEngine()); if ($this->document) { $engine->setDocument($this->document); } $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine($engine) ->setNavigation($this->buildSideNav()); return $this->delegateToController($controller); } public function buildSideNav($for_app = false) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $engine = id(new LegalpadDocumentSignatureSearchEngine()) - ->setViewer($user); + ->setViewer($viewer); if ($this->document) { $engine->setDocument($this->document); } $engine->addNavigationItems($nav->getMenu()); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); if ($this->document) { $crumbs->addTextCrumb( $this->document->getMonogram(), '/'.$this->document->getMonogram()); $crumbs->addTextCrumb( pht('Manage'), $this->getApplicationURI('view/'.$this->document->getID().'/')); } else { $crumbs->addTextCrumb( pht('Signatures'), '/legalpad/signatures/'); } return $crumbs; } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php index a940eb646f..b50b91e896 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php @@ -1,97 +1,91 @@ code = $data['code']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $code = $request->getURIData('code'); // NOTE: We're using the omnipotent user to handle logged-out signatures // and corporate signatures. $signature = id(new LegalpadDocumentSignatureQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withSecretKeys(array($this->code)) + ->withSecretKeys(array($code)) ->executeOne(); if (!$signature) { return $this->newDialog() ->setTitle(pht('Unable to Verify Signature')) ->appendParagraph( pht( 'The signature verification code is incorrect, or the signature '. 'has been invalidated. Make sure you followed the link in the '. 'email correctly.')) ->addCancelButton('/', pht('Rats!')); } if ($signature->isVerified()) { return $this->newDialog() ->setTitle(pht('Signature Already Verified')) ->appendParagraph(pht('This signature has already been verified.')) ->addCancelButton('/', pht('Okay')); } if ($request->isFormPost()) { $signature ->setVerified(LegalpadDocumentSignature::VERIFIED) ->save(); return $this->newDialog() ->setTitle(pht('Signature Verified')) ->appendParagraph(pht('The signature is now verified.')) ->addCancelButton('/', pht('Okay')); } $document_link = phutil_tag( 'a', array( 'href' => '/'.$signature->getDocument()->getMonogram(), 'target' => '_blank', ), $signature->getDocument()->getTitle()); $signed_at = phabricator_datetime($signature->getDateCreated(), $viewer); $name = $signature->getSignerName(); $email = $signature->getSignerEmail(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht('Please verify this document signature.')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Document')) ->setValue($document_link)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Signed At')) ->setValue($signed_at)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Name')) ->setValue($name)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Email')) ->setValue($email)); return $this->newDialog() ->setTitle(pht('Verify Signature?')) ->appendChild($form->buildLayoutView()) ->addCancelButton('/') ->addSubmitButton(pht('Verify Signature')); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php index 6d58d1b693..6f2c447b71 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php @@ -1,112 +1,106 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $signature = id(new LegalpadDocumentSignatureQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$signature) { return new Aphront404Response(); } // NOTE: In order to see signature details (which include the relatively // internal-feeling "notes" field) you must be able to edit the document. // Essentially, this power is for document managers. Notably, this prevents // users from seeing notes about their own exemptions by guessing their // signature ID. This is purely a policy check. $document = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->withIDs(array($signature->getDocument()->getID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$document) { return new Aphront404Response(); } $document_id = $signature->getDocument()->getID(); $next_uri = $this->getApplicationURI('signatures/'.$document_id.'/'); $data = $signature->getSignatureData(); $exemption_phid = $signature->getExemptionPHID(); $actor_phid = idx($data, 'actorPHID'); $handles = $this->loadViewerHandles( array( $exemption_phid, $actor_phid, )); $exemptor_handle = $handles[$exemption_phid]; $actor_handle = $handles[$actor_phid]; $form = id(new AphrontFormView()) ->setUser($viewer); if ($signature->getExemptionPHID()) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Exemption By')) ->setValue($exemptor_handle->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Notes')) ->setValue(idx($data, 'notes'))); } $type_corporation = LegalpadDocument::SIGNATURE_TYPE_CORPORATION; if ($signature->getSignatureType() == $type_corporation) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Signing User')) ->setValue($actor_handle->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Company Name')) ->setValue(idx($data, 'name'))) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Address')) ->setValue(phutil_escape_html_newlines(idx($data, 'address')))) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Contact Name')) ->setValue(idx($data, 'contact.name'))) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Contact Email')) ->setValue( phutil_tag( 'a', array( 'href' => 'mailto:'.idx($data, 'email'), ), idx($data, 'email')))); } return $this->newDialog() ->setTitle(pht('Signature Details')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($form->buildLayoutView()) ->addCancelButton($next_uri, pht('Close')); } } diff --git a/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php b/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php new file mode 100644 index 0000000000..22d41449ad --- /dev/null +++ b/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php @@ -0,0 +1,133 @@ +getAdapter(); + + $edgetype_legal = LegalpadObjectNeedsSignatureEdgeType::EDGECONST; + $current = $adapter->loadEdgePHIDs($edgetype_legal); + + $allowed_types = array( + PhabricatorLegalpadDocumentPHIDType::TYPECONST, + ); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + $object = $adapter->getObject(); + $author_phid = $object->getAuthorPHID(); + + $signatures = id(new LegalpadDocumentSignatureQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withDocumentPHIDs($phids) + ->withSignerPHIDs(array($author_phid)) + ->execute(); + $signatures = mpull($signatures, null, 'getDocumentPHID'); + + $signed = array(); + foreach ($phids as $phid) { + if (isset($signatures[$phid])) { + $signed[] = $phid; + unset($phids[$phid]); + } + } + + if ($signed) { + $this->logEffect(self::DO_SIGNED, $phids); + } + + if (!$phids) { + return; + } + + $xaction = $adapter->newTransaction() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $edgetype_legal) + ->setNewValue( + array( + '+' => $phids, + )); + + $adapter->queueTransaction($xaction); + + $this->logEffect(self::DO_REQUIRED, $phids); + } + + protected function getActionEffectMap() { + return array( + self::DO_SIGNED => array( + 'icon' => 'fa-terminal', + 'color' => 'green', + 'name' => pht('Already Signed'), + ), + self::DO_REQUIRED => array( + 'icon' => 'fa-terminal', + 'color' => 'green', + 'name' => pht('Required Signature'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_SIGNED: + return pht( + '%s document(s) are already signed: %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_REQUIRED: + return pht( + 'Required %s signature(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + } + } + + public function getHeraldActionName() { + return pht('Require signatures'); + } + + public function supportsRuleType($rule_type) { + return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); + } + + public function applyEffect($object, HeraldEffect $effect) { + return $this->applyRequire($effect->getTarget()); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new LegalpadDocumentDatasource(); + } + + public function renderActionDescription($value) { + return pht( + 'Require document signatures: %s.', + $this->renderHandleList($value)); + } +} diff --git a/src/applications/macro/controller/PhabricatorMacroAudioController.php b/src/applications/macro/controller/PhabricatorMacroAudioController.php index e46b7c78e8..92b98e5564 100644 --- a/src/applications/macro/controller/PhabricatorMacroAudioController.php +++ b/src/applications/macro/controller/PhabricatorMacroAudioController.php @@ -1,159 +1,153 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { $this->requireApplicationCapability( PhabricatorMacroManageCapability::CAPABILITY); - $request = $this->getRequest(); - $viewer = $request->getUser(); - $macro = id(new PhabricatorMacroQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } $errors = array(); $view_uri = $this->getApplicationURI('/view/'.$macro->getID().'/'); $e_file = null; $file = null; if ($request->isFormPost()) { $xactions = array(); if ($request->getBool('behaviorForm')) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType( PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR) ->setNewValue($request->getStr('audioBehavior')); } else { $file = null; if ($request->getFileExists('file')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'name' => $request->getStr('name'), 'authorPHID' => $viewer->getPHID(), 'isExplicitUpload' => true, )); } if ($file) { if (!$file->isAudio()) { $errors[] = pht('You must upload audio.'); $e_file = pht('Invalid'); } else { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransaction::TYPE_AUDIO) ->setNewValue($file->getPHID()); } } else { $errors[] = pht('You must upload an audio file.'); $e_file = pht('Required'); } } if (!$errors) { id(new PhabricatorMacroEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($macro, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } } $form = id(new AphrontFormView()) ->addHiddenInput('behaviorForm', 1) ->setUser($viewer); $options = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Audio Behavior')) ->setName('audioBehavior') ->setValue( nonempty( $macro->getAudioBehavior(), PhabricatorFileImageMacro::AUDIO_BEHAVIOR_NONE)); $options->addButton( PhabricatorFileImageMacro::AUDIO_BEHAVIOR_NONE, pht('Do Not Play'), pht('Do not play audio.')); $options->addButton( PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE, pht('Play Once'), pht('Play audio once, when the viewer looks at the macro.')); $options->addButton( PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP, pht('Play Continuously'), pht( 'Play audio continuously, treating the macro as an audio source. '. 'Best for ambient sounds.')); $form->appendChild($options); $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Audio Behavior')) ->addCancelButton($view_uri)); $crumbs = $this->buildApplicationCrumbs(); $title = pht('Edit Audio Behavior'); $crumb = pht('Edit Audio'); $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); $crumbs->addTextCrumb($crumb, $request->getRequestURI()); $upload_form = id(new AphrontFormView()) ->setEncType('multipart/form-data') ->setUser($viewer) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('Audio File')) ->setName('file')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Upload File'))); $upload = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Audio')) ->setForm($upload_form); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload, ), array( 'title' => $title, )); } } diff --git a/src/applications/macro/controller/PhabricatorMacroCommentController.php b/src/applications/macro/controller/PhabricatorMacroCommentController.php index f438e9b31a..f038140be1 100644 --- a/src/applications/macro/controller/PhabricatorMacroCommentController.php +++ b/src/applications/macro/controller/PhabricatorMacroCommentController.php @@ -1,69 +1,63 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $macro = id(new PhabricatorMacroQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $view_uri = $this->getApplicationURI('/view/'.$macro->getID().'/'); $xactions = array(); $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorMacroTransactionComment()) ->setContent($request->getStr('comment'))); $editor = id(new PhabricatorMacroEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($macro, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } } diff --git a/src/applications/macro/controller/PhabricatorMacroDisableController.php b/src/applications/macro/controller/PhabricatorMacroDisableController.php index 0804e119e5..a9868647f0 100644 --- a/src/applications/macro/controller/PhabricatorMacroDisableController.php +++ b/src/applications/macro/controller/PhabricatorMacroDisableController.php @@ -1,62 +1,56 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { $this->requireApplicationCapability( PhabricatorMacroManageCapability::CAPABILITY); - $request = $this->getRequest(); - $user = $request->getUser(); - $macro = id(new PhabricatorMacroQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } - $view_uri = $this->getApplicationURI('/view/'.$this->id.'/'); + $view_uri = $this->getApplicationURI('/view/'.$id.'/'); if ($request->isDialogFormPost() || $macro->getIsDisabled()) { $xaction = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransaction::TYPE_DISABLED) ->setNewValue($macro->getIsDisabled() ? 0 : 1); $editor = id(new PhabricatorMacroEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($macro, array($xaction)); return id(new AphrontRedirectResponse())->setURI($view_uri); } $dialog = new AphrontDialogView(); $dialog ->setUser($request->getUser()) ->setTitle(pht('Really disable macro?')) ->appendChild( phutil_tag( 'p', array(), pht( 'Really disable the much-beloved image macro %s? '. 'It will be sorely missed.', $macro->getName()))) - ->setSubmitURI($this->getApplicationURI('/disable/'.$this->id.'/')) + ->setSubmitURI($this->getApplicationURI('/disable/'.$id.'/')) ->addSubmitButton(pht('Disable')) ->addCancelButton($view_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/macro/controller/PhabricatorMacroEditController.php b/src/applications/macro/controller/PhabricatorMacroEditController.php index f15e5364c2..9e988ef637 100644 --- a/src/applications/macro/controller/PhabricatorMacroEditController.php +++ b/src/applications/macro/controller/PhabricatorMacroEditController.php @@ -1,295 +1,289 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { $this->requireApplicationCapability( PhabricatorMacroManageCapability::CAPABILITY); - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->id) { + if ($id) { $macro = id(new PhabricatorMacroQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needFiles(true) ->executeOne(); if (!$macro) { return new Aphront404Response(); } } else { $macro = new PhabricatorFileImageMacro(); - $macro->setAuthorPHID($user->getPHID()); + $macro->setAuthorPHID($viewer->getPHID()); } $errors = array(); $e_name = true; $e_file = null; $file = null; if ($request->isFormPost()) { $original = clone $macro; $new_name = null; if ($request->getBool('name_form') || !$macro->getID()) { $new_name = $request->getStr('name'); $macro->setName($new_name); if (!strlen($macro->getName())) { $errors[] = pht('Macro name is required.'); $e_name = pht('Required'); } else if (!preg_match('/^[a-z0-9:_-]{3,}\z/', $macro->getName())) { $errors[] = pht( 'Macro must be at least three characters long and contain only '. 'lowercase letters, digits, hyphens, colons and underscores.'); $e_name = pht('Invalid'); } else { $e_name = null; } } $uri = $request->getStr('url'); $engine = new PhabricatorDestructionEngine(); $file = null; if ($request->getFileExists('file')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'name' => $request->getStr('name'), - 'authorPHID' => $user->getPHID(), + 'authorPHID' => $viewer->getPHID(), 'isExplicitUpload' => true, 'canCDN' => true, )); } else if ($uri) { try { // Rate limit outbound fetches to make this mechanism less useful for // scanning networks and ports. PhabricatorSystemActionEngine::willTakeAction( - array($user->getPHID()), + array($viewer->getPHID()), new PhabricatorFilesOutboundRequestAction(), 1); $file = PhabricatorFile::newFromFileDownload( $uri, array( 'name' => $request->getStr('name'), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 'isExplicitUpload' => true, 'canCDN' => true, )); if (!$file->isViewableInBrowser()) { $mime_type = $file->getMimeType(); $engine->destroyObject($file); $file = null; throw new Exception( pht( 'The URI "%s" does not correspond to a valid image file, got '. 'a file with MIME type "%s". You must specify the URI of a '. 'valid image file.', $uri, $mime_type)); } else { $file - ->setAuthorPHID($user->getPHID()) + ->setAuthorPHID($viewer->getPHID()) ->save(); } } catch (HTTPFutureHTTPResponseStatus $status) { $errors[] = pht( 'The URI "%s" could not be loaded, got %s error.', $uri, $status->getStatusCode()); } catch (Exception $ex) { $errors[] = $ex->getMessage(); } } else if ($request->getStr('phid')) { $file = id(new PhabricatorFileQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($request->getStr('phid'))) ->executeOne(); } if ($file) { if (!$file->isViewableInBrowser()) { $errors[] = pht('You must upload an image.'); $e_file = pht('Invalid'); } else { $macro->setFilePHID($file->getPHID()); $macro->attachFile($file); $e_file = null; } } if (!$macro->getID() && !$file) { $errors[] = pht('You must upload an image to create a macro.'); $e_file = pht('Required'); } if (!$errors) { try { $xactions = array(); if ($new_name !== null) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransaction::TYPE_NAME) ->setNewValue($new_name); } if ($file) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransaction::TYPE_FILE) ->setNewValue($file->getPHID()); } $editor = id(new PhabricatorMacroEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($original, $xactions); $view_uri = $this->getApplicationURI('/view/'.$original->getID().'/'); return id(new AphrontRedirectResponse())->setURI($view_uri); } catch (AphrontDuplicateKeyQueryException $ex) { throw $ex; $errors[] = pht('Macro name is not unique!'); $e_name = pht('Duplicate'); } } } $current_file = null; if ($macro->getFilePHID()) { $current_file = $macro->getFile(); } $form = new AphrontFormView(); $form->addHiddenInput('name_form', 1); $form->setUser($request->getUser()); $form ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($macro->getName()) ->setCaption( pht('This word or phrase will be replaced with the image.')) ->setError($e_name)); if (!$macro->getID()) { if ($current_file) { $current_file_view = id(new PhabricatorFileLinkView()) ->setFilePHID($current_file->getPHID()) ->setFileName($current_file->getName()) ->setFileViewable(true) ->setFileViewURI($current_file->getBestURI()) ->render(); $form->addHiddenInput('phid', $current_file->getPHID()); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Selected File')) ->setValue($current_file_view)); $other_label = pht('Change File'); } else { $other_label = pht('File'); } $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URL')) ->setName('url') ->setValue($request->getStr('url')) ->setError($request->getFileExists('file') ? false : $e_file)); $form->appendChild( id(new AphrontFormFileControl()) ->setLabel($other_label) ->setName('file') ->setError($request->getStr('url') ? false : $e_file)); } $view_uri = $this->getApplicationURI('/view/'.$macro->getID().'/'); if ($macro->getID()) { $cancel_uri = $view_uri; } else { $cancel_uri = $this->getApplicationURI(); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Image Macro')) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); if ($macro->getID()) { $title = pht('Edit Image Macro'); $crumb = pht('Edit Macro'); $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); } else { $title = pht('Create Image Macro'); $crumb = pht('Create Macro'); } $crumbs->addTextCrumb($crumb, $request->getRequestURI()); $upload = null; if ($macro->getID()) { $upload_form = id(new AphrontFormView()) ->setEncType('multipart/form-data') ->setUser($request->getUser()); $upload_form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URL')) ->setName('url') ->setValue($request->getStr('url'))); $upload_form ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Upload File'))); $upload = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New File')) ->setForm($upload_form); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload, ), array( 'title' => $title, )); } } diff --git a/src/applications/macro/controller/PhabricatorMacroListController.php b/src/applications/macro/controller/PhabricatorMacroListController.php index 730f8de3b1..3847117275 100644 --- a/src/applications/macro/controller/PhabricatorMacroListController.php +++ b/src/applications/macro/controller/PhabricatorMacroListController.php @@ -1,24 +1,20 @@ key = idx($data, 'key'); - } + public function handleRequest(AphrontRequest $request) { + $key = $request->getURIData('key'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->key) + ->setQueryKey($key) ->setSearchEngine(new PhabricatorMacroSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/macro/controller/PhabricatorMacroMemeController.php b/src/applications/macro/controller/PhabricatorMacroMemeController.php index ff25a8a8a4..03b13f6205 100644 --- a/src/applications/macro/controller/PhabricatorMacroMemeController.php +++ b/src/applications/macro/controller/PhabricatorMacroMemeController.php @@ -1,69 +1,68 @@ getRequest(); + public function handleRequest(AphrontRequest $request) { $macro_name = $request->getStr('macro'); $upper_text = $request->getStr('uppertext'); $lower_text = $request->getStr('lowertext'); - $user = $request->getUser(); + $viewer = $request->getViewer(); - $uri = self::generateMacro($user, $macro_name, + $uri = self::generateMacro($viewer, $macro_name, $upper_text, $lower_text); if ($uri === false) { return new Aphront404Response(); } return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($uri); } - public static function generateMacro($user, $macro_name, $upper_text, + public static function generateMacro($viewer, $macro_name, $upper_text, $lower_text) { $macro = id(new PhabricatorMacroQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withNames(array($macro_name)) ->needFiles(true) ->executeOne(); if (!$macro) { return false; } $file = $macro->getFile(); $upper_text = strtoupper($upper_text); $lower_text = strtoupper($lower_text); $mixed_text = md5($upper_text).':'.md5($lower_text); $hash = 'meme'.hash('sha256', $mixed_text); $xform = id(new PhabricatorTransformedFile()) ->loadOneWhere('originalphid=%s and transform=%s', $file->getPHID(), $hash); if ($xform) { $memefile = id(new PhabricatorFileQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($xform->getTransformedPHID())) ->executeOne(); if ($memefile) { return $memefile->getBestURI(); } } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $transformers = (new PhabricatorImageTransformer()); $newfile = $transformers ->executeMemeTransform($file, $upper_text, $lower_text); $xfile = new PhabricatorTransformedFile(); $xfile->setOriginalPHID($file->getPHID()); $xfile->setTransformedPHID($newfile->getPHID()); $xfile->setTransform($hash); $xfile->save(); return $newfile->getBestURI(); } } diff --git a/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php b/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php index 25a223f5ac..5851cc5ad8 100644 --- a/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php +++ b/src/applications/macro/controller/PhabricatorMacroMemeDialogController.php @@ -1,77 +1,76 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $phid = head($request->getArr('macro')); $above = $request->getStr('above'); $below = $request->getStr('below'); $e_macro = true; $errors = array(); if ($request->isDialogFormPost()) { if (!$phid) { $e_macro = pht('Required'); $errors[] = pht('Macro name is required.'); } else { $macro = id(new PhabricatorMacroQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$macro) { $e_macro = pht('Invalid'); $errors[] = pht('No such macro.'); } } if (!$errors) { $options = new PhutilSimpleOptions(); $data = array( 'src' => $macro->getName(), 'above' => $above, 'below' => $below, ); $string = $options->unparse($data, $escape = '}'); $result = array( 'text' => "{meme, {$string}}", ); return id(new AphrontAjaxResponse())->setContent($result); } } $view = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Macro')) ->setName('macro') ->setLimit(1) ->setDatasource(new PhabricatorMacroDatasource()) ->setError($e_macro)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Above')) ->setName('above') ->setValue($above)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Below')) ->setName('below') ->setValue($below)); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Create Meme')) ->appendForm($view) ->addCancelButton('/') ->addSubmitButton(pht('Llama Diorama')); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/macro/controller/PhabricatorMacroViewController.php b/src/applications/macro/controller/PhabricatorMacroViewController.php index a031c7f92b..8915954fc4 100644 --- a/src/applications/macro/controller/PhabricatorMacroViewController.php +++ b/src/applications/macro/controller/PhabricatorMacroViewController.php @@ -1,181 +1,175 @@ id = $data['id']; - } - public function shouldAllowPublic() { return true; } - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $macro = id(new PhabricatorMacroQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needFiles(true) ->executeOne(); if (!$macro) { return new Aphront404Response(); } $file = $macro->getFile(); $title_short = pht('Macro "%s"', $macro->getName()); $title_long = pht('Image Macro "%s"', $macro->getName()); $actions = $this->buildActionView($macro); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $title_short, $this->getApplicationURI('/view/'.$macro->getID().'/')); $properties = $this->buildPropertyView($macro, $actions); if ($file) { $file_view = new PHUIPropertyListView(); $file_view->addImageContent( phutil_tag( 'img', array( 'src' => $file->getViewURI(), 'class' => 'phabricator-image-macro-hero', ))); } $timeline = $this->buildTransactionTimeline( $macro, new PhabricatorMacroTransactionQuery()); $header = id(new PHUIHeaderView()) - ->setUser($user) + ->setUser($viewer) ->setPolicyObject($macro) ->setHeader($title_long); if (!$macro->getIsDisabled()) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { $header->setStatus('fa-ban', 'red', pht('Archived')); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $comment_header = $is_serious ? pht('Add Comment') : pht('Grovel in Awe'); - $draft = PhabricatorDraft::newFromUserAndKey($user, $macro->getPHID()); + $draft = PhabricatorDraft::newFromUserAndKey($viewer, $macro->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setObjectPHID($macro->getPHID()) ->setDraft($draft) ->setHeaderText($comment_header) ->setAction($this->getApplicationURI('/comment/'.$macro->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); if ($file_view) { $object_box->addPropertyList($file_view); } return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, $add_comment_form, ), array( 'title' => $title_short, )); } private function buildActionView(PhabricatorFileImageMacro $macro) { $can_manage = $this->hasApplicationCapability( PhabricatorMacroManageCapability::CAPABILITY); $request = $this->getRequest(); $view = id(new PhabricatorActionListView()) ->setUser($request->getUser()) ->setObject($macro) ->setObjectURI($request->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Macro')) ->setHref($this->getApplicationURI('/edit/'.$macro->getID().'/')) ->setDisabled(!$can_manage) ->setWorkflow(!$can_manage) ->setIcon('fa-pencil')); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Audio')) ->setHref($this->getApplicationURI('/audio/'.$macro->getID().'/')) ->setDisabled(!$can_manage) ->setWorkflow(!$can_manage) ->setIcon('fa-music')); if ($macro->getIsDisabled()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Macro')) ->setHref($this->getApplicationURI('/disable/'.$macro->getID().'/')) ->setWorkflow(true) ->setDisabled(!$can_manage) ->setIcon('fa-check')); } else { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Macro')) ->setHref($this->getApplicationURI('/disable/'.$macro->getID().'/')) ->setWorkflow(true) ->setDisabled(!$can_manage) ->setIcon('fa-ban')); } return $view; } private function buildPropertyView( PhabricatorFileImageMacro $macro, PhabricatorActionListView $actions) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()) ->setObject($macro) ->setActionList($actions); switch ($macro->getAudioBehavior()) { case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE: $view->addProperty(pht('Audio Behavior'), pht('Play Once')); break; case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP: $view->addProperty(pht('Audio Behavior'), pht('Loop')); break; } $audio_phid = $macro->getAudioPHID(); if ($audio_phid) { $view->addProperty( pht('Audio'), $viewer->renderHandle($audio_phid)); } $view->invokeWillRenderEvent(); return $view; } } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 0d68bf0c1b..72a6577924 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -1,143 +1,154 @@ file = $file; return $this; } public function getFile() { return $this->assertAttached($this->file); } public function attachAudio(PhabricatorFile $audio = null) { $this->audio = $audio; return $this; } public function getAudio() { return $this->assertAttached($this->audio); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'authorPHID' => 'phid?', 'isDisabled' => 'bool', 'audioPHID' => 'phid?', 'audioBehavior' => 'text64', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'name' => array( 'columns' => array('name'), 'unique' => true, ), 'key_disabled' => array( 'columns' => array('isDisabled'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorMacroMacroPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorMacroEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorMacroTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } +/* -( PhabricatorTokenRecevierInterface )---------------------------------- */ + + + public function getUsersToNotifyOfTokenGiven() { + return array( + $this->getAuthorPHID(), + ); + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php index ad659af259..d4adee9570 100644 --- a/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php +++ b/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php @@ -1,69 +1,69 @@ pht('No such Maniphest task exists.'), 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'), 'ERR-NO-EFFECT' => pht('Update has no effect.'), ); } protected function defineParamTypes() { return $this->getTaskFields($is_new = false); } protected function defineReturnType() { return 'nonempty dict'; } protected function execute(ConduitAPIRequest $request) { $id = $request->getValue('id'); $phid = $request->getValue('phid'); if (($id && $phid) || (!$id && !$phid)) { throw new Exception( pht( "Specify exactly one of '%s' and '%s'.", 'id', 'phid')); } - $query = id (new ManiphestTaskQuery()) + $query = id(new ManiphestTaskQuery()) ->setViewer($request->getUser()) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true); if ($id) { $query->withIDs(array($id)); } else { $query->withPHIDs(array($phid)); } $task = $query->executeOne(); $params = $request->getAllParameters(); unset($params['id']); unset($params['phid']); if (call_user_func_array('coalesce', $params) === null) { throw new ConduitException('ERR-NO-EFFECT'); } if (!$task) { throw new ConduitException('ERR-BAD-TASK'); } $task = $this->applyRequest($task, $request, $is_new = false); return $this->buildTaskInfoDictionary($task); } } diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php index 6db04a6e32..88e6849afb 100644 --- a/src/applications/maniphest/controller/ManiphestExportController.php +++ b/src/applications/maniphest/controller/ManiphestExportController.php @@ -1,141 +1,133 @@ key = $data['key']; - return $this; - } - /** * @phutil-external-symbol class PHPExcel * @phutil-external-symbol class PHPExcel_IOFactory * @phutil-external-symbol class PHPExcel_Style_NumberFormat * @phutil-external-symbol class PHPExcel_Cell_DataType */ - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $key = $request->getURIData('key'); $ok = @include_once 'PHPExcel.php'; if (!$ok) { - $dialog = new AphrontDialogView(); - $dialog->setUser($user); + $dialog = $this->newDialog(); $inst1 = pht( 'This system does not have PHPExcel installed. This software '. 'component is required to export tasks to Excel. Have your system '. 'administrator install it from:'); $inst2 = pht( 'Your PHP "%s" needs to be updated to include the '. 'PHPExcel Classes directory.', 'include_path'); $dialog->setTitle(pht('Excel Export Not Configured')); $dialog->appendChild(hsprintf( '

%s

'. '
'. '

'. 'http://www.phpexcel.net/'. '

'. '
'. '

%s

', $inst1, $inst2)); $dialog->addCancelButton('/maniphest/'); return id(new AphrontDialogResponse())->setDialog($dialog); } // TODO: PHPExcel has a dependency on the PHP zip extension. We should test // for that here, since it fatals if we don't have the ZipArchive class. $saved = id(new PhabricatorSavedQueryQuery()) - ->setViewer($user) - ->withQueryKeys(array($this->key)) + ->setViewer($viewer) + ->withQueryKeys(array($key)) ->executeOne(); if (!$saved) { $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($user); - if ($engine->isBuiltinQuery($this->key)) { - $saved = $engine->buildSavedQueryFromBuiltin($this->key); + ->setViewer($viewer); + if ($engine->isBuiltinQuery($key)) { + $saved = $engine->buildSavedQueryFromBuiltin($key); } if (!$saved) { return new Aphront404Response(); } } $formats = ManiphestExcelFormat::loadAllFormats(); $export_formats = array(); foreach ($formats as $format_class => $format_object) { $export_formats[$format_class] = $format_object->getName(); } if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); - $dialog->setUser($user); + $dialog->setUser($viewer); $dialog->setTitle(pht('Export Tasks to Excel')); $dialog->appendChild( phutil_tag( 'p', array(), pht('Do you want to export the query results to Excel?'))); $form = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Format:')) ->setName('excel-format') ->setOptions($export_formats)); $dialog->appendChild($form); $dialog->addCancelButton('/maniphest/'); $dialog->addSubmitButton(pht('Export to Excel')); return id(new AphrontDialogResponse())->setDialog($dialog); } $format = idx($formats, $request->getStr('excel-format')); if ($format === null) { throw new Exception(pht('Excel format object not found.')); } $saved->makeEphemeral(); $saved->setParameter('limit', PHP_INT_MAX); $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($user); + ->setViewer($viewer); $query = $engine->buildQueryFromSavedQuery($saved); - $query->setViewer($user); + $query->setViewer($viewer); $tasks = $query->execute(); $all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs')); $all_assigned = mpull($tasks, 'getOwnerPHID'); $handles = id(new PhabricatorHandleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array_merge($all_projects, $all_assigned)) ->execute(); $workbook = new PHPExcel(); - $format->buildWorkbook($workbook, $tasks, $handles, $user); + $format->buildWorkbook($workbook, $tasks, $handles, $viewer); $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); ob_start(); $writer->save('php://output'); $data = ob_get_clean(); $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; return id(new AphrontFileResponse()) ->setMimeType($mime) ->setDownload($format->getFileName().'.xlsx') ->setContent($data); } } diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index bdf446150e..6c2ce9fec3 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -1,794 +1,790 @@ view = idx($data, 'view'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + 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); $window = $request->getStr('set_window'); $uri = $uri->alter('window', $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(); } $nav->appendChild($core); $nav->setCrumbs( $this->buildApplicationCrumbs() ->addTextCrumb(pht('Reports'))); return $this->buildApplicationPage( $nav, array( 'title' => pht('Maniphest Reports'), 'device' => false, )); } public function renderBurn() { $request = $this->getRequest(); - $user = $request->getUser(); + $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'); $joins = ''; 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); } $data = queryfx_all( $conn, 'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q WHERE transactionType = %s ORDER BY x.dateCreated ASC', $table->getTableName(), $joins, ManiphestTransaction::TYPE_STATUS); $stats = array(); $day_buckets = array(); $open_tasks = array(); foreach ($data as $key => $row) { // NOTE: Hack to avoid json_decode(). $oldv = trim($row['oldValue'], '"'); $newv = trim($row['newValue'], '"'); 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'], - $user, + $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, - $user, + $viewer, 'YW'); if ($week_bucket != $last_week) { if ($week) { $rows[] = $this->formatBurnRow( - pht('Week of %s', phabricator_date($last_week_epoch, $user)), + 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, - $user, + $viewer, 'Ym'); if ($month_bucket != $last_month) { if ($month) { $rows[] = $this->formatBurnRow( - phabricator_format_local_time($last_month_epoch, $user, 'F, Y'), + 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, $user), $info); + $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('raphael-core'); require_celerity_resource('raphael-g'); require_celerity_resource('raphael-g-line'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, 'x' => array( $burn_x, ), 'y' => array( $burn_y, ), 'xformat' => 'epoch', 'yformat' => 'int', )); return array($filter, $chart, $panel); } private function renderReportFilters(array $tokens, $has_window) { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $form = id(new AphrontFormView()) - ->setUser($user) + ->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(); - $user = $request->getUser(); + $viewer = $request->getUser(); $query = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->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(), $user); + $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, $user); + $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(); - $user = $request->getUser(); + $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, $user); + $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 casues "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/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php index 44b13955fb..25920212c8 100644 --- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php +++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php @@ -1,68 +1,67 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); if (!$request->validateCSRF()) { return new Aphront403Response(); } $task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($request->getInt('task'))) ->needProjectPHIDs(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$task) { return new Aphront404Response(); } if ($request->getInt('after')) { $after_task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($request->getInt('after'))) ->executeOne(); if (!$after_task) { return new Aphront404Response(); } list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( $after_task, $is_after = true); } else { list($pri, $sub) = ManiphestTransactionEditor::getEdgeSubpriority( $request->getInt('priority'), $is_end = false); } $xactions = array(); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) ->setNewValue($pri); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) ->setNewValue($sub); $editor = id(new ManiphestTransactionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($task, $xactions); return id(new AphrontAjaxResponse())->setContent( array( 'tasks' => $this->renderSingleTask($task), )); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 0d4fa74398..8619987201 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,588 +1,583 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); $e_title = null; $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $task = id(new ManiphestTaskQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needSubscriberPHIDs(true) ->executeOne(); if (!$task) { return new Aphront404Response(); } $workflow = $request->getStr('workflow'); $parent_task = null; if ($workflow && is_numeric($workflow)) { $parent_task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($workflow)) ->executeOne(); } $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); $field_list - ->setViewer($user) + ->setViewer($viewer) ->readFieldsFromStorage($task); $e_commit = ManiphestTaskHasCommitEdgeType::EDGECONST; $e_dep_on = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $e_dep_by = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $e_rev = ManiphestTaskHasRevisionEdgeType::EDGECONST; $e_mock = ManiphestTaskHasMockEdgeType::EDGECONST; $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes( array( $e_commit, $e_dep_on, $e_dep_by, $e_rev, $e_mock, )); $edges = idx($query->execute(), $phid); $phids = array_fill_keys($query->getDestinationPHIDs(), true); if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $attached = $task->getAttached(); foreach ($attached as $type => $list) { foreach ($list as $phid => $info) { $phids[$phid] = true; } } if ($parent_task) { $phids[$parent_task->getPHID()] = true; } $phids = array_keys($phids); - $handles = $user->loadHandles($phids); + $handles = $viewer->loadHandles($phids); $info_view = null; if ($parent_task) { $info_view = new PHUIInfoView(); $info_view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $info_view->addButton( id(new PHUIButtonView()) ->setTag('a') ->setHref('/maniphest/task/create/?parent='.$parent_task->getID()) ->setText(pht('Create Another Subtask'))); $info_view->appendChild(hsprintf( 'Created a subtask of %s.', $handles->renderHandle($parent_task->getPHID()))); } else if ($workflow == 'create') { $info_view = new PHUIInfoView(); $info_view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $info_view->addButton( id(new PHUIButtonView()) ->setTag('a') ->setHref('/maniphest/task/create/?template='.$task->getID()) ->setText(pht('Similar Task'))); $info_view->addButton( id(new PHUIButtonView()) ->setTag('a') ->setHref('/maniphest/task/create/') ->setText(pht('Empty Task'))); $info_view->appendChild(pht('New task created. Create another?')); } $engine = new PhabricatorMarkupEngine(); - $engine->setViewer($user); + $engine->setViewer($viewer); $engine->setContextObject($task); $engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION); $timeline = $this->buildTransactionTimeline( $task, new ManiphestTransactionQuery(), $engine); $resolution_types = ManiphestTaskStatus::getTaskStatusMap(); $transaction_types = array( PhabricatorTransactions::TYPE_COMMENT => pht('Comment'), ManiphestTransaction::TYPE_STATUS => pht('Change Status'), ManiphestTransaction::TYPE_OWNER => pht('Reassign / Claim'), PhabricatorTransactions::TYPE_SUBSCRIBERS => pht('Add CCs'), ManiphestTransaction::TYPE_PRIORITY => pht('Change Priority'), PhabricatorTransactions::TYPE_EDGE => pht('Associate Projects'), ); // Remove actions the user doesn't have permission to take. $requires = array( ManiphestTransaction::TYPE_OWNER => ManiphestEditAssignCapability::CAPABILITY, ManiphestTransaction::TYPE_PRIORITY => ManiphestEditPriorityCapability::CAPABILITY, PhabricatorTransactions::TYPE_EDGE => ManiphestEditProjectsCapability::CAPABILITY, ManiphestTransaction::TYPE_STATUS => ManiphestEditStatusCapability::CAPABILITY, ); foreach ($transaction_types as $type => $name) { if (isset($requires[$type])) { if (!$this->hasApplicationCapability($requires[$type])) { unset($transaction_types[$type]); } } } // Don't show an option to change to the current status, or to change to // the duplicate status explicitly. unset($resolution_types[$task->getStatus()]); unset($resolution_types[ManiphestTaskStatus::getDuplicateStatus()]); // Don't show owner/priority changes for closed tasks, as they don't make // much sense. if ($task->isClosed()) { unset($transaction_types[ManiphestTransaction::TYPE_PRIORITY]); unset($transaction_types[ManiphestTransaction::TYPE_OWNER]); } $default_claim = array( - $user->getPHID() => $user->getUsername().' ('.$user->getRealName().')', + $viewer->getPHID() => $viewer->getUsername(). + ' ('.$viewer->getRealName().')', ); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', - $user->getPHID(), + $viewer->getPHID(), $task->getPHID()); if ($draft) { $draft_text = $draft->getDraft(); } else { $draft_text = null; } $projects_source = new PhabricatorProjectDatasource(); $users_source = new PhabricatorPeopleDatasource(); $mailable_source = new PhabricatorMetaMTAMailableDatasource(); $comment_form = new AphrontFormView(); $comment_form - ->setUser($user) + ->setUser($viewer) ->setWorkflow(true) ->setAction('/maniphest/transaction/save/') ->setEncType('multipart/form-data') ->addHiddenInput('taskID', $task->getID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setOptions($transaction_types) ->setID('transaction-action')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('resolution') ->setControlID('resolution') ->setControlStyle('display: none') ->setOptions($resolution_types)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Assign To')) ->setName('assign_to') ->setControlID('assign_to') ->setControlStyle('display: none') ->setID('assign-tokenizer') ->setDisableBehavior(true) ->setDatasource($users_source)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CCs')) ->setName('ccs') ->setControlID('ccs') ->setControlStyle('display: none') ->setID('cc-tokenizer') ->setDisableBehavior(true) ->setDatasource($mailable_source)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Priority')) ->setName('priority') ->setOptions($priority_map) ->setControlID('priority') ->setControlStyle('display: none') ->setValue($task->getPriority())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setControlID('projects') ->setControlStyle('display: none') ->setID('projects-tokenizer') ->setDisableBehavior(true) ->setDatasource($projects_source)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file') ->setControlID('file') ->setControlStyle('display: none')) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setUser($user) + ->setUser($viewer) ->setLabel(pht('Comments')) ->setName('comments') ->setValue($draft_text) ->setID('transaction-comments') - ->setUser($user)) + ->setUser($viewer)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Submit'))); $control_map = array( ManiphestTransaction::TYPE_STATUS => 'resolution', ManiphestTransaction::TYPE_OWNER => 'assign_to', PhabricatorTransactions::TYPE_SUBSCRIBERS => 'ccs', ManiphestTransaction::TYPE_PRIORITY => 'priority', PhabricatorTransactions::TYPE_EDGE => 'projects', ); $tokenizer_map = array( PhabricatorTransactions::TYPE_EDGE => array( 'id' => 'projects-tokenizer', 'src' => $projects_source->getDatasourceURI(), 'placeholder' => $projects_source->getPlaceholderText(), ), ManiphestTransaction::TYPE_OWNER => array( 'id' => 'assign-tokenizer', 'src' => $users_source->getDatasourceURI(), 'value' => $default_claim, 'limit' => 1, 'placeholder' => $users_source->getPlaceholderText(), ), PhabricatorTransactions::TYPE_SUBSCRIBERS => array( 'id' => 'cc-tokenizer', 'src' => $mailable_source->getDatasourceURI(), 'placeholder' => $mailable_source->getPlaceholderText(), ), ); // TODO: Initializing these behaviors for logged out users fatals things. - if ($user->isLoggedIn()) { + if ($viewer->isLoggedIn()) { Javelin::initBehavior('maniphest-transaction-controls', array( 'select' => 'transaction-action', 'controlMap' => $control_map, 'tokenizers' => $tokenizer_map, )); Javelin::initBehavior('maniphest-transaction-preview', array( 'uri' => '/maniphest/transaction/preview/'.$task->getID().'/', 'preview' => 'transaction-preview', 'comments' => 'transaction-comments', 'action' => 'transaction-action', 'map' => $control_map, 'tokenizers' => $tokenizer_map, )); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $comment_header = $is_serious ? pht('Add Comment') : pht('Weigh In'); $preview_panel = phutil_tag_div( 'aphront-panel-preview', phutil_tag( 'div', array('id' => 'transaction-preview'), phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')))); $object_name = 'T'.$task->getID(); $actions = $this->buildActionView($task); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($object_name, '/'.$object_name); $header = $this->buildHeaderView($task); $properties = $this->buildPropertyView( $task, $field_list, $edges, $actions, $handles); $description = $this->buildDescriptionView($task, $engine); - if (!$user->isLoggedIn()) { + if (!$viewer->isLoggedIn()) { // TODO: Eventually, everything should run through this. For now, we're // only using it to get a consistent "Login to Comment" button. $comment_box = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setRequestURI($request->getRequestURI()); $preview_panel = null; } else { $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText($comment_header) ->setForm($comment_form); $timeline->setQuoteTargetID('transaction-comments'); $timeline->setQuoteRef($object_name); } $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); if ($description) { $object_box->addPropertyList($description); } return $this->buildApplicationPage( array( $crumbs, $info_view, $object_box, $timeline, $comment_box, $preview_panel, ), array( 'title' => 'T'.$task->getID().' '.$task->getTitle(), 'pageObjects' => array($task->getPHID()), )); } private function buildHeaderView(ManiphestTask $task) { $view = id(new PHUIHeaderView()) ->setHeader($task->getTitle()) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); $status = $task->getStatus(); $status_name = ManiphestTaskStatus::renderFullDescription($status); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); return $view; } private function buildActionView(ManiphestTask $task) { $viewer = $this->getRequest()->getUser(); $viewer_phid = $viewer->getPHID(); $id = $task->getID(); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($task) ->setObjectURI($this->getRequest()->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/task/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Merge Duplicates In')) ->setHref("/search/attach/{$phid}/TASK/merge/") ->setWorkflow(true) ->setIcon('fa-compress') ->setDisabled(!$can_edit) ->setWorkflow(true)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($this->getApplicationURI("/task/create/?parent={$id}")) ->setIcon('fa-level-down')); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Blocking Tasks')) ->setHref("/search/attach/{$phid}/TASK/blocks/") ->setWorkflow(true) ->setIcon('fa-link') ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, PhabricatorActionListView $actions, $handles) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($task) ->setActionList($actions); $view->addProperty( pht('Assigned To'), $task->getOwnerPHID() ? $handles->renderHandle($task->getOwnerPHID()) : phutil_tag('em', array(), pht('None'))); $view->addProperty( pht('Priority'), ManiphestTaskPriority::getTaskPriorityName($task->getPriority())); $view->addProperty( pht('Author'), $handles->renderHandle($task->getAuthorPHID())); $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( ManiphestTaskDependedOnByTaskEdgeType::EDGECONST => pht('Blocks'), ManiphestTaskDependsOnTaskEdgeType::EDGECONST => pht('Blocked By'), ManiphestTaskHasRevisionEdgeType::EDGECONST => pht('Differential Revisions'), ManiphestTaskHasMockEdgeType::EDGECONST => pht('Pholio Mocks'), ); $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); $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->renderLink($revision_handle->getName()), $revisions_commits[$phid]); } } } foreach ($edge_types as $edge_type => $edge_name) { if ($edges[$edge_type]) { $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); $view->addProperty( $edge_name, $edge_handles->renderList()); } } if ($revisions_commits) { $view->addProperty( pht('Commits'), phutil_implode_html(phutil_tag('br'), $revisions_commits)); } $attached = $task->getAttached(); if (!is_array($attached)) { $attached = array(); } $file_infos = idx($attached, PhabricatorFileFilePHIDType::TYPECONST); if ($file_infos) { $file_phids = array_keys($file_infos); // TODO: These should probably be handles or something; clean this up // as we sort out file attachments. $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $file_view = new PhabricatorFileLinkListView(); $file_view->setFiles($files); $view->addProperty( pht('Files'), $file_view->render()); } $view->invokeWillRenderEvent(); $field_list->appendFieldsToPropertyList( $task, $viewer, $view); return $view; } private function buildDescriptionView( ManiphestTask $task, PhabricatorMarkupEngine $engine) { $section = null; if (strlen($task->getDescription())) { $section = new PHUIPropertyListView(); $section->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $section->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION))); } return $section; } } diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 72b75c876f..703bb61768 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -1,761 +1,755 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); $response_type = $request->getStr('responseType', 'task'); $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); $can_edit_assign = $this->hasApplicationCapability( ManiphestEditAssignCapability::CAPABILITY); $can_edit_policies = $this->hasApplicationCapability( ManiphestEditPoliciesCapability::CAPABILITY); $can_edit_priority = $this->hasApplicationCapability( ManiphestEditPriorityCapability::CAPABILITY); $can_edit_projects = $this->hasApplicationCapability( ManiphestEditProjectsCapability::CAPABILITY); $can_edit_status = $this->hasApplicationCapability( ManiphestEditStatusCapability::CAPABILITY); $can_create_projects = PhabricatorPolicyFilter::hasCapability( - $user, + $viewer, PhabricatorApplication::getByClass('PhabricatorProjectApplication'), ProjectCreateProjectsCapability::CAPABILITY); $parent_task = null; $template_id = null; - if ($this->id) { + if ($id) { $task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->executeOne(); if (!$task) { return new Aphront404Response(); } } else { - $task = ManiphestTask::initializeNewTask($user); + $task = ManiphestTask::initializeNewTask($viewer); // We currently do not allow you to set the task status when creating // a new task, although now that statuses are custom it might make // sense. $can_edit_status = false; // These allow task creation with defaults. if (!$request->isFormPost()) { $task->setTitle($request->getStr('title')); if ($can_edit_projects) { $projects = $request->getStr('projects'); if ($projects) { $tokens = $request->getStrList('projects'); $type_project = PhabricatorProjectProjectPHIDType::TYPECONST; foreach ($tokens as $key => $token) { if (phid_get_type($token) == $type_project) { // If this is formatted like a PHID, leave it as-is. continue; } if (preg_match('/^#/', $token)) { // If this already has a "#", leave it as-is. continue; } // Add a "#" prefix. $tokens[$key] = '#'.$token; } $default_projects = id(new PhabricatorObjectQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withNames($tokens) ->execute(); $default_projects = mpull($default_projects, 'getPHID'); if ($default_projects) { $task->attachProjectPHIDs($default_projects); } } } if ($can_edit_priority) { $priority = $request->getInt('priority'); if ($priority !== null) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if (isset($priority_map[$priority])) { $task->setPriority($priority); } } } $task->setDescription($request->getStr('description')); if ($can_edit_assign) { $assign = $request->getStr('assign'); if (strlen($assign)) { $assign_user = id(new PhabricatorPeopleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withUsernames(array($assign)) ->executeOne(); if (!$assign_user) { $assign_user = id(new PhabricatorPeopleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($assign)) ->executeOne(); } if ($assign_user) { $task->setOwnerPHID($assign_user->getPHID()); } } } } $template_id = $request->getInt('template'); // You can only have a parent task if you're creating a new task. $parent_id = $request->getInt('parent'); if (strlen($parent_id)) { $parent_task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($parent_id)) ->executeOne(); if (!$parent_task) { return new Aphront404Response(); } if (!$template_id) { $template_id = $parent_id; } } } $errors = array(); $e_title = true; $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); - $field_list->setViewer($user); + $field_list->setViewer($viewer); $field_list->readFieldsFromStorage($task); $aux_fields = $field_list->getFields(); $v_space = $task->getSpacePHID(); if ($request->isFormPost()) { $changes = array(); $new_title = $request->getStr('title'); $new_desc = $request->getStr('description'); $new_status = $request->getStr('status'); $v_space = $request->getStr('spacePHID'); if (!$task->getID()) { $workflow = 'create'; } else { $workflow = ''; } $changes[ManiphestTransaction::TYPE_TITLE] = $new_title; $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc; if ($can_edit_status) { $changes[ManiphestTransaction::TYPE_STATUS] = $new_status; } else if (!$task->getID()) { // Create an initial status transaction for the burndown chart. // TODO: We can probably remove this once Facts comes online. $changes[ManiphestTransaction::TYPE_STATUS] = $task->getStatus(); } $owner_tokenizer = $request->getArr('assigned_to'); $owner_phid = reset($owner_tokenizer); if (!strlen($new_title)) { $e_title = pht('Required'); $errors[] = pht('Title is required.'); } $old_values = array(); foreach ($aux_fields as $aux_arr_key => $aux_field) { // TODO: This should be buildFieldTransactionsFromRequest() once we // switch to ApplicationTransactions properly. $aux_old_value = $aux_field->getOldValueForApplicationTransactions(); $aux_field->readValueFromRequest($request); $aux_new_value = $aux_field->getNewValueForApplicationTransactions(); // TODO: We're faking a call to the ApplicaitonTransaction validation // logic here. We need valid objects to pass, but they aren't used // in a meaningful way. For now, build User objects. Once the Maniphest // objects exist, this will switch over automatically. This is a big // hack but shouldn't be long for this world. $placeholder_editor = new PhabricatorUserProfileEditor(); $field_errors = $aux_field->validateApplicationTransactions( $placeholder_editor, PhabricatorTransactions::TYPE_CUSTOMFIELD, array( id(new ManiphestTransaction()) ->setOldValue($aux_old_value) ->setNewValue($aux_new_value), )); foreach ($field_errors as $error) { $errors[] = $error->getMessage(); } $old_values[$aux_field->getFieldKey()] = $aux_old_value; } if ($errors) { $task->setTitle($new_title); $task->setDescription($new_desc); $task->setPriority($request->getInt('priority')); $task->setOwnerPHID($owner_phid); $task->attachSubscriberPHIDs($request->getArr('cc')); $task->attachProjectPHIDs($request->getArr('projects')); } else { if ($can_edit_priority) { $changes[ManiphestTransaction::TYPE_PRIORITY] = $request->getInt('priority'); } if ($can_edit_assign) { $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; } $changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] = array('=' => $request->getArr('cc')); if ($can_edit_projects) { $projects = $request->getArr('projects'); $changes[PhabricatorTransactions::TYPE_EDGE] = $projects; $column_phid = $request->getStr('columnPHID'); // allow for putting a task in a project column at creation -only- if (!$task->getID() && $column_phid && $projects) { $column = id(new PhabricatorProjectColumnQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withProjectPHIDs($projects) ->withPHIDs(array($column_phid)) ->executeOne(); if ($column) { $changes[ManiphestTransaction::TYPE_PROJECT_COLUMN] = array( 'new' => array( 'projectPHID' => $column->getProjectPHID(), 'columnPHIDs' => array($column_phid), ), 'old' => array( 'projectPHID' => $column->getProjectPHID(), 'columnPHIDs' => array(), ), ); } } } if ($can_edit_policies) { $changes[PhabricatorTransactions::TYPE_SPACE] = $v_space; $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = $request->getStr('viewPolicy'); $changes[PhabricatorTransactions::TYPE_EDIT_POLICY] = $request->getStr('editPolicy'); } $template = new ManiphestTransaction(); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); if ($type == ManiphestTransaction::TYPE_PROJECT_COLUMN) { $transaction->setNewValue($value['new']); $transaction->setOldValue($value['old']); } else if ($type == PhabricatorTransactions::TYPE_EDGE) { $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $transaction ->setMetadataValue('edge:type', $project_type) ->setNewValue( array( '=' => array_fuse($value), )); } else { $transaction->setNewValue($value); } $transactions[] = $transaction; } if ($aux_fields) { foreach ($aux_fields as $aux_field) { $transaction = clone $template; $transaction->setTransactionType( PhabricatorTransactions::TYPE_CUSTOMFIELD); $aux_key = $aux_field->getFieldKey(); $transaction->setMetadataValue('customfield:key', $aux_key); $old = idx($old_values, $aux_key); $new = $aux_field->getNewValueForApplicationTransactions(); $transaction->setOldValue($old); $transaction->setNewValue($new); $transactions[] = $transaction; } } if ($transactions) { $is_new = !$task->getID(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); - $event->setUser($user); + $event->setUser($viewer); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = id(new ManiphestTransactionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($task, $transactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); - $event->setUser($user); + $event->setUser($viewer); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); } if ($parent_task) { // TODO: This should be transactional now. id(new PhabricatorEdgeEditor()) ->addEdge( $parent_task->getPHID(), ManiphestTaskDependsOnTaskEdgeType::EDGECONST, $task->getPHID()) ->save(); $workflow = $parent_task->getID(); } if ($request->isAjax()) { switch ($response_type) { case 'card': $owner = null; if ($task->getOwnerPHID()) { $owner = id(new PhabricatorHandleQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($task->getOwnerPHID())) ->executeOne(); } $tasks = id(new ProjectBoardTaskCard()) - ->setViewer($user) + ->setViewer($viewer) ->setTask($task) ->setOwner($owner) ->setCanEdit(true) ->getItem(); $column = id(new PhabricatorProjectColumnQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($request->getStr('columnPHID'))) ->executeOne(); if (!$column) { return new Aphront404Response(); } // re-load projects for accuracy as they are not re-loaded via // the editor $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $task->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $task->attachProjectPHIDs($project_phids); $remove_from_board = false; if (!in_array($column->getProjectPHID(), $project_phids)) { $remove_from_board = true; } $positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withColumns(array($column)) ->execute(); $task_phids = mpull($positions, 'getObjectPHID'); $column_tasks = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs($task_phids) ->execute(); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { // TODO: This is a little bit awkward, because PHP and JS use // slightly different sort order parameters to achieve the same // effect. It would be good to unify this a bit at some point. $sort_map = array(); foreach ($positions as $position) { $sort_map[$position->getObjectPHID()] = array( -$position->getSequence(), $position->getID(), ); } } else { $sort_map = mpull( $column_tasks, 'getPrioritySortVector', 'getPHID'); } $data = array( 'sortMap' => $sort_map, 'removeFromBoard' => $remove_from_board, ); break; case 'task': default: $tasks = $this->renderSingleTask($task); $data = array(); break; } return id(new AphrontAjaxResponse())->setContent( array( 'tasks' => $tasks, 'data' => $data, )); } $redirect_uri = '/T'.$task->getID(); if ($workflow) { $redirect_uri .= '?workflow='.$workflow; } return id(new AphrontRedirectResponse()) ->setURI($redirect_uri); } } else { if (!$task->getID()) { $task->attachSubscriberPHIDs(array( - $user->getPHID(), + $viewer->getPHID(), )); if ($template_id) { $template_task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($template_id)) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->executeOne(); if ($template_task) { $cc_phids = array_unique(array_merge( $template_task->getSubscriberPHIDs(), - array($user->getPHID()))); + array($viewer->getPHID()))); $task->attachSubscriberPHIDs($cc_phids); $task->attachProjectPHIDs($template_task->getProjectPHIDs()); $task->setOwnerPHID($template_task->getOwnerPHID()); $task->setPriority($template_task->getPriority()); $task->setViewPolicy($template_task->getViewPolicy()); $task->setEditPolicy($template_task->getEditPolicy()); $v_space = $template_task->getSpacePHID(); $template_fields = PhabricatorCustomField::getObjectFields( $template_task, PhabricatorCustomField::ROLE_EDIT); $fields = $template_fields->getFields(); foreach ($fields as $key => $field) { if (!$field->shouldCopyWhenCreatingSimilarTask()) { unset($fields[$key]); } if (empty($aux_fields[$key])) { unset($fields[$key]); } } if ($fields) { id(new PhabricatorCustomFieldList($fields)) - ->setViewer($user) + ->setViewer($viewer) ->readFieldsFromStorage($template_task); foreach ($fields as $key => $field) { $aux_fields[$key]->setValueFromStorage( $field->getValueForStorage()); } } } } } } $error_view = null; if ($errors) { $error_view = new PHUIInfoView(); $error_view->setErrors($errors); } $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if ($task->getOwnerPHID()) { $assigned_value = array($task->getOwnerPHID()); } else { $assigned_value = array(); } if ($task->getSubscriberPHIDs()) { $cc_value = $task->getSubscriberPHIDs(); } else { $cc_value = array(); } if ($task->getProjectPHIDs()) { $projects_value = $task->getProjectPHIDs(); } else { $projects_value = array(); } $cancel_id = nonempty($task->getID(), $template_id); if ($cancel_id) { $cancel_uri = '/T'.$cancel_id; } else { $cancel_uri = '/maniphest/'; } if ($task->getID()) { $button_name = pht('Save Task'); $header_name = pht('Edit Task'); } else if ($parent_task) { $cancel_uri = '/T'.$parent_task->getID(); $button_name = pht('Create Task'); $header_name = pht('Create New Subtask'); } else { $button_name = pht('Create Task'); $header_name = pht('Create New Task'); } require_celerity_resource('maniphest-task-edit-css'); $project_tokenizer_id = celerity_generate_unique_node_id(); $form = new AphrontFormView(); $form - ->setUser($user) + ->setUser($viewer) ->addHiddenInput('template', $template_id) ->addHiddenInput('responseType', $response_type) ->addHiddenInput('order', $order) ->addHiddenInput('ungrippable', $request->getStr('ungrippable')) ->addHiddenInput('columnPHID', $request->getStr('columnPHID')); if ($parent_task) { $form ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Parent Task')) - ->setValue($user->renderHandle($parent_task->getPHID()))) + ->setValue($viewer->renderHandle($parent_task->getPHID()))) ->addHiddenInput('parent', $parent_task->getID()); } $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Title')) ->setName('title') ->setError($e_title) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($task->getTitle())); if ($can_edit_status) { // See T4819. $status_map = ManiphestTaskStatus::getTaskStatusMap(); $dup_status = ManiphestTaskStatus::getDuplicateStatus(); if ($task->getStatus() != $dup_status) { unset($status_map[$dup_status]); } $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setValue($task->getStatus()) ->setOptions($status_map)); } $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($task) ->execute(); if ($can_edit_assign) { $form->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Assigned To')) ->setName('assigned_to') ->setValue($assigned_value) - ->setUser($user) + ->setUser($viewer) ->setDatasource(new PhabricatorPeopleDatasource()) ->setLimit(1)); } $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CC')) ->setName('cc') ->setValue($cc_value) - ->setUser($user) + ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource())); if ($can_edit_priority) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Priority')) ->setName('priority') ->setOptions($priority_map) ->setValue($task->getPriority())); } if ($can_edit_policies) { $form ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($task) ->setPolicies($policies) ->setSpacePHID($v_space) ->setName('viewPolicy')) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($task) ->setPolicies($policies) ->setName('editPolicy')); } if ($can_edit_projects) { $caption = null; if ($can_create_projects) { $caption = javelin_tag( 'a', array( 'href' => '/project/create/', 'mustcapture' => true, 'sigil' => 'project-create', ), pht('Create New Project')); } $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($projects_value) ->setID($project_tokenizer_id) ->setCaption($caption) ->setDatasource(new PhabricatorProjectDatasource())); } $field_list->appendFieldsToForm($form); require_celerity_resource('phui-info-view-css'); Javelin::initBehavior('project-create', array( 'tokenizerID' => $project_tokenizer_id, )); $description_control = id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setID('description-textarea') ->setValue($task->getDescription()) - ->setUser($user); + ->setUser($viewer); $form ->appendChild($description_control); if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle($header_name) ->appendChild( array( $error_view, $form->buildLayoutView(), )) ->addCancelButton($cancel_uri) ->addSubmitButton($button_name); return id(new AphrontDialogResponse())->setDialog($dialog); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button_name)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($header_name) ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Description Preview')) ->setControlID('description-textarea') ->setPreviewURI($this->getApplicationURI('task/descriptionpreview/')); if ($task->getID()) { $page_objects = array($task->getPHID()); } else { $page_objects = array(); } $crumbs = $this->buildApplicationCrumbs(); if ($task->getID()) { $crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID()); } $crumbs->addTextCrumb($header_name); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => $header_name, 'pageObjects' => $page_objects, )); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskListController.php b/src/applications/maniphest/controller/ManiphestTaskListController.php index 9d2fa33716..ddb2827e83 100644 --- a/src/applications/maniphest/controller/ManiphestTaskListController.php +++ b/src/applications/maniphest/controller/ManiphestTaskListController.php @@ -1,27 +1,23 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine( id(new ManiphestTaskSearchEngine()) ->setShowBatchControls(true)) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php index 0d7988cd64..65756ca8e4 100644 --- a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php +++ b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php @@ -1,135 +1,128 @@ id = $data['id']; - } - - public function processRequest() { - - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); $comments = $request->getStr('comments'); $task = id(new ManiphestTaskQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$task) { return new Aphront404Response(); } id(new PhabricatorDraft()) - ->setAuthorPHID($user->getPHID()) + ->setAuthorPHID($viewer->getPHID()) ->setDraftKey($task->getPHID()) ->setDraft($comments) ->replaceOrDelete(); $action = $request->getStr('action'); $transaction = new ManiphestTransaction(); - $transaction->setAuthorPHID($user->getPHID()); + $transaction->setAuthorPHID($viewer->getPHID()); $transaction->setTransactionType($action); // This should really be split into a separate transaction, but it should // all come out in the wash once we fully move to modern stuff. $transaction->attachComment( id(new ManiphestTransactionComment()) ->setContent($comments)); $value = $request->getStr('value'); // grab phids for handles and set transaction values based on action and // value (empty or control-specific format) coming in from the wire switch ($action) { case ManiphestTransaction::TYPE_PRIORITY: $transaction->setOldValue($task->getPriority()); $transaction->setNewValue($value); break; case ManiphestTransaction::TYPE_OWNER: if ($value) { $value = current(json_decode($value)); $phids = array($value); } else { $phids = array(); } $transaction->setNewValue($value); break; case PhabricatorTransactions::TYPE_SUBSCRIBERS: if ($value) { $value = json_decode($value); } if (!$value) { $value = array(); } $phids = array(); foreach ($value as $cc_phid) { $phids[] = $cc_phid; } $transaction->setOldValue(array()); $transaction->setNewValue($phids); break; case PhabricatorTransactions::TYPE_EDGE: if ($value) { $value = phutil_json_decode($value); } if (!$value) { $value = array(); } $phids = array(); $value = array_fuse($value); foreach ($value as $project_phid) { $phids[] = $project_phid; $value[$project_phid] = array('dst' => $project_phid); } $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $transaction ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $project_type) ->setOldValue(array()) ->setNewValue($value); break; case ManiphestTransaction::TYPE_STATUS: $phids = array(); $transaction->setOldValue($task->getStatus()); $transaction->setNewValue($value); break; default: $phids = array(); $transaction->setNewValue($value); break; } - $phids[] = $user->getPHID(); + $phids[] = $viewer->getPHID(); $handles = $this->loadViewerHandles($phids); $transactions = array(); $transactions[] = $transaction; $engine = new PhabricatorMarkupEngine(); - $engine->setViewer($user); + $engine->setViewer($viewer); $engine->setContextObject($task); if ($transaction->hasComment()) { $engine->addObject( $transaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } $engine->process(); $transaction->setHandles($handles); $view = id(new PhabricatorApplicationTransactionView()) - ->setUser($user) + ->setUser($viewer) ->setTransactions($transactions) ->setIsPreview(true); return id(new AphrontAjaxResponse()) ->setContent((string)phutil_implode_html('', $view->buildEvents())); } } diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php index f7693d9416..b060b18cab 100644 --- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php +++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php @@ -1,210 +1,209 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $task = id(new ManiphestTaskQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($request->getStr('taskID'))) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->executeOne(); if (!$task) { return new Aphront404Response(); } $task_uri = '/'.$task->getMonogram(); $transactions = array(); $action = $request->getStr('action'); $implicit_ccs = array(); $explicit_ccs = array(); $transaction = new ManiphestTransaction(); $transaction ->setTransactionType($action); switch ($action) { case ManiphestTransaction::TYPE_STATUS: $transaction->setNewValue($request->getStr('resolution')); break; case ManiphestTransaction::TYPE_OWNER: $assign_to = $request->getArr('assign_to'); $assign_to = reset($assign_to); $transaction->setNewValue($assign_to); break; case PhabricatorTransactions::TYPE_EDGE: $projects = $request->getArr('projects'); $projects = array_merge($projects, $task->getProjectPHIDs()); $projects = array_filter($projects); $projects = array_unique($projects); $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $transaction ->setMetadataValue('edge:type', $project_type) ->setNewValue( array( '+' => array_fuse($projects), )); break; case PhabricatorTransactions::TYPE_SUBSCRIBERS: // Accumulate the new explicit CCs into the array that we'll add in // the CC transaction later. $explicit_ccs = $request->getArr('ccs'); // Throw away the primary transaction. $transaction = null; break; case ManiphestTransaction::TYPE_PRIORITY: $transaction->setNewValue($request->getInt('priority')); break; case PhabricatorTransactions::TYPE_COMMENT: // Nuke this, we're going to create it below. $transaction = null; break; default: throw new Exception(pht("Unknown action '%s'!", $action)); } if ($transaction) { $transactions[] = $transaction; } // When you interact with a task, we add you to the CC list so you get // further updates, and possibly assign the task to you if you took an // ownership action (closing it) but it's currently unowned. We also move // previous owners to CC if ownership changes. Detect all these conditions // and create side-effect transactions for them. $implicitly_claimed = false; if ($action == ManiphestTransaction::TYPE_OWNER) { if ($task->getOwnerPHID() == $transaction->getNewValue()) { // If this is actually no-op, don't generate the side effect. } else { // Otherwise, when a task is reassigned, move the previous owner to CC. if ($task->getOwnerPHID()) { $implicit_ccs[] = $task->getOwnerPHID(); } } } if ($action == ManiphestTransaction::TYPE_STATUS) { $resolution = $request->getStr('resolution'); if (!$task->getOwnerPHID() && ManiphestTaskStatus::isClosedStatus($resolution)) { // Closing an unassigned task. Assign the user as the owner of // this task. $assign = new ManiphestTransaction(); $assign->setTransactionType(ManiphestTransaction::TYPE_OWNER); - $assign->setNewValue($user->getPHID()); + $assign->setNewValue($viewer->getPHID()); $transactions[] = $assign; $implicitly_claimed = true; } } $user_owns_task = false; if ($implicitly_claimed) { $user_owns_task = true; } else { if ($action == ManiphestTransaction::TYPE_OWNER) { - if ($transaction->getNewValue() == $user->getPHID()) { + if ($transaction->getNewValue() == $viewer->getPHID()) { $user_owns_task = true; } - } else if ($task->getOwnerPHID() == $user->getPHID()) { + } else if ($task->getOwnerPHID() == $viewer->getPHID()) { $user_owns_task = true; } } if (!$user_owns_task) { // If we aren't making the user the new task owner and they aren't the // existing task owner, add them to CC unless they're aleady CC'd. - if (!in_array($user->getPHID(), $task->getSubscriberPHIDs())) { - $implicit_ccs[] = $user->getPHID(); + if (!in_array($viewer->getPHID(), $task->getSubscriberPHIDs())) { + $implicit_ccs[] = $viewer->getPHID(); } } if ($implicit_ccs || $explicit_ccs) { // TODO: These implicit CC rules should probably be handled inside the // Editor, eventually. $all_ccs = array_fuse($implicit_ccs) + array_fuse($explicit_ccs); $cc_transaction = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('+' => $all_ccs)); if (!$explicit_ccs) { $cc_transaction->setIgnoreOnNoEffect(true); } $transactions[] = $cc_transaction; } $comments = $request->getStr('comments'); if (strlen($comments) || !$transactions) { $transactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent($comments)); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => false, 'transactions' => $transactions, )); - $event->setUser($user); + $event->setUser($viewer); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = id(new ManiphestTransactionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect($request->isContinueRequest()); try { $editor->applyTransactions($task, $transactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($task_uri) ->setException($ex); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', - $user->getPHID(), + $viewer->getPHID(), $task->getPHID()); if ($draft) { $draft->delete(); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => false, 'transactions' => $transactions, )); - $event->setUser($user); + $event->setUser($viewer); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); return id(new AphrontRedirectResponse())->setURI($task_uri); } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 39111644cb..dcc518e06b 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,737 +1,720 @@ getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: if ($this->getIsNewObject()) { return null; } return (int)$object->getPriority(); case ManiphestTransaction::TYPE_STATUS: if ($this->getIsNewObject()) { return null; } return $object->getStatus(); case ManiphestTransaction::TYPE_TITLE: if ($this->getIsNewObject()) { return null; } return $object->getTitle(); case ManiphestTransaction::TYPE_DESCRIPTION: if ($this->getIsNewObject()) { return null; } return $object->getDescription(); case ManiphestTransaction::TYPE_OWNER: return nonempty($object->getOwnerPHID(), null); case ManiphestTransaction::TYPE_PROJECT_COLUMN: // These are pre-populated. return $xaction->getOldValue(); case ManiphestTransaction::TYPE_SUBPRIORITY: return $object->getSubpriority(); case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: return null; } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: return (int)$xaction->getNewValue(); case ManiphestTransaction::TYPE_OWNER: return nonempty($xaction->getNewValue(), null); case ManiphestTransaction::TYPE_STATUS: case ManiphestTransaction::TYPE_TITLE: case ManiphestTransaction::TYPE_DESCRIPTION: case ManiphestTransaction::TYPE_SUBPRIORITY: case ManiphestTransaction::TYPE_PROJECT_COLUMN: case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: case ManiphestTransaction::TYPE_UNBLOCK: return $xaction->getNewValue(); } } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PROJECT_COLUMN: $new_column_phids = $new['columnPHIDs']; $old_column_phids = $old['columnPHIDs']; sort($new_column_phids); sort($old_column_phids); return ($old !== $new); } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: return $object->setPriority($xaction->getNewValue()); case ManiphestTransaction::TYPE_STATUS: return $object->setStatus($xaction->getNewValue()); case ManiphestTransaction::TYPE_TITLE: return $object->setTitle($xaction->getNewValue()); case ManiphestTransaction::TYPE_DESCRIPTION: return $object->setDescription($xaction->getNewValue()); case ManiphestTransaction::TYPE_OWNER: $phid = $xaction->getNewValue(); // Update the "ownerOrdering" column to contain the full name of the // owner, if the task is assigned. $handle = null; if ($phid) { $handle = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($phid)) ->executeOne(); } if ($handle) { $object->setOwnerOrdering($handle->getName()); } else { $object->setOwnerOrdering(null); } return $object->setOwnerPHID($phid); case ManiphestTransaction::TYPE_SUBPRIORITY: $object->setSubpriority($xaction->getNewValue()); return; case ManiphestTransaction::TYPE_PROJECT_COLUMN: // these do external (edge) updates return; case ManiphestTransaction::TYPE_MERGED_INTO: $object->setStatus(ManiphestTaskStatus::getDuplicateStatus()); return; case ManiphestTransaction::TYPE_MERGED_FROM: return; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PROJECT_COLUMN: $board_phid = idx($xaction->getNewValue(), 'projectPHID'); if (!$board_phid) { throw new Exception( pht( "Expected '%s' in column transaction.", 'projectPHID')); } $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array()); $new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array()); if (count($new_phids) !== 1) { throw new Exception( pht( "Expected exactly one '%s' in column transaction.", 'columnPHIDs')); } $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($this->requireActor()) ->withPHIDs($new_phids) ->execute(); $columns = mpull($columns, null, 'getPHID'); $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($this->requireActor()) ->withObjectPHIDs(array($object->getPHID())) ->withBoardPHIDs(array($board_phid)) ->execute(); $before_phid = idx($xaction->getNewValue(), 'beforePHID'); $after_phid = idx($xaction->getNewValue(), 'afterPHID'); if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) { // If we are not moving the object between columns and also not // reordering the position, this is a move on some other order // (like priority). We can leave the positions untouched and just // bail, there's no work to be done. return; } // Otherwise, we're either moving between columns or adjusting the // object's position in the "natural" ordering, so we do need to update // some rows. // Remove all existing column positions on the board. foreach ($positions as $position) { $position->delete(); } // Add the new column positions. foreach ($new_phids as $phid) { $column = idx($columns, $phid); if (!$column) { throw new Exception( pht('No such column "%s" exists!', $phid)); } // Load the other object positions in the column. Note that we must // skip implicit column creation to avoid generating a new position // if the target column is a backlog column. $other_positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($this->requireActor()) ->withColumns(array($column)) ->withBoardPHIDs(array($board_phid)) ->setSkipImplicitCreate(true) ->execute(); $other_positions = msort($other_positions, 'getOrderingKey'); // Set up the new position object. We're going to figure out the // right sequence number and then persist this object with that // sequence number. $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($column->getPHID()) ->setObjectPHID($object->getPHID()); $updates = array(); $sequence = 0; // If we're just dropping this into the column without any specific // position information, put it at the top. if (!$before_phid && !$after_phid) { $new_position->setSequence($sequence)->save(); $sequence++; } foreach ($other_positions as $position) { $object_phid = $position->getObjectPHID(); // If this is the object we're moving before and we haven't // saved yet, insert here. if (($before_phid == $object_phid) && !$new_position->getID()) { $new_position->setSequence($sequence)->save(); $sequence++; } // This object goes here in the sequence; we might need to update // the row. if ($sequence != $position->getSequence()) { $updates[$position->getID()] = $sequence; } $sequence++; // If this is the object we're moving after and we haven't saved // yet, insert here. if (($after_phid == $object_phid) && !$new_position->getID()) { $new_position->setSequence($sequence)->save(); $sequence++; } } // We should have found a place to put it. if (!$new_position->getID()) { throw new Exception( pht('Unable to find a place to insert object on column!')); } // If we changed other objects' column positions, bulk reorder them. if ($updates) { $position = new PhabricatorProjectColumnPosition(); $conn_w = $position->establishConnection('w'); $pairs = array(); foreach ($updates as $id => $sequence) { // 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, $sequence); } queryfx( $conn_w, 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', $position->getTableName(), implode(', ', $pairs)); } } break; default: 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 ManiphestTransaction::TYPE_STATUS: $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) { $unblock_xactions = array(); $unblock_xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK) ->setOldValue(array($object->getPHID() => $old)) ->setNewValue(array($object->getPHID() => $new)); id(new ManiphestTransactionEditor()) ->setActor($this->getActor()) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($blocked_task, $unblock_xactions); } } } return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } 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 the tasks a task is blocked by 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}") ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addTextSection( pht('TASK DESCRIPTION'), $object->getDescription()); } $body->addLinkSection( pht('TASK DETAIL'), PhabricatorEnv::getProductionURI('/T'.$object->getID())); $board_phids = array(); $type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_column) { $new = $xaction->getNewValue(); $project_phid = idx($new, 'projectPHID'); if ($project_phid) { $board_phids[] = $project_phid; } } } 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().'/')); } } return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldSendMail($object, $xactions); } 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 didApplyHeraldRules( - PhabricatorLiskDAO $object, - HeraldAdapter $adapter, - HeraldTranscript $transcript) { - - $xactions = array(); - - $assign_phid = $adapter->getAssignPHID(); - if ($assign_phid) { - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_OWNER) - ->setNewValue($assign_phid); - } - - return $xactions; - } - protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { parent::requireCapabilities($object, $xaction); $app_capability_map = array( ManiphestTransaction::TYPE_PRIORITY => ManiphestEditPriorityCapability::CAPABILITY, ManiphestTransaction::TYPE_STATUS => ManiphestEditStatusCapability::CAPABILITY, ManiphestTransaction::TYPE_OWNER => ManiphestEditAssignCapability::CAPABILITY, PhabricatorTransactions::TYPE_EDIT_POLICY => ManiphestEditPoliciesCapability::CAPABILITY, PhabricatorTransactions::TYPE_VIEW_POLICY => ManiphestEditPoliciesCapability::CAPABILITY, ); $transaction_type = $xaction->getTransactionType(); $app_capability = null; if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) { switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: $app_capability = ManiphestEditProjectsCapability::CAPABILITY; break; } } else { $app_capability = idx($app_capability_map, $transaction_type); } if ($app_capability) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($this->getActor()) ->withClasses(array('PhabricatorManiphestApplication')) ->executeOne(); PhabricatorPolicyFilter::requireCapability( $this->getActor(), $app, $app_capability); } } protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { $copy = parent::adjustObjectForPolicyChecks($object, $xactions); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_OWNER: $copy->setOwnerPHID($xaction->getNewValue()); break; default: continue; } } 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, $allow_recursion = true) { $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 = 0.01; // If the adjacent task has a subpriority that is identical or very // close to the task we're looking at, we're going to move it and all // tasks with the same subpriority a little farther down the subpriority // scale. if ($allow_recursion && (abs($adjacent->getSubpriority() - $base) < $epsilon)) { $conn_w = $adjacent->establishConnection('w'); $min = ($adjacent->getSubpriority() - ($epsilon)); $max = ($adjacent->getSubpriority() + ($epsilon)); // Get all of the tasks with the similar subpriorities to the adjacent // task, including the adjacent task itself. $query = id(new ManiphestTaskQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPriorities(array($adjacent->getPriority())) ->withSubpriorityBetween($min, $max); if (!$is_after) { $query->setOrderVector(array('-priority', '-subpriority', '-id')); } else { $query->setOrderVector(array('priority', 'subpriority', 'id')); } $shift_all = $query->execute(); $shift_last = last($shift_all); // Select the most extreme subpriority in the result set as the // base value. $shift_base = head($shift_all)->getSubpriority(); // Find the subpriority before or after the task at the end of the // block. list($shift_pri, $shift_sub) = self::getAdjacentSubpriority( $shift_last, $is_after, $allow_recursion = false); $delta = ($shift_sub - $shift_base); $count = count($shift_all); $shift = array(); $cursor = 1; foreach ($shift_all as $shift_task) { $shift_target = $shift_base + (($cursor / $count) * $delta); $cursor++; queryfx( $conn_w, 'UPDATE %T SET subpriority = %f WHERE id = %d', $adjacent->getTableName(), $shift_target, $shift_task->getID()); // If we're shifting the adjacent task, update it. if ($shift_task->getID() == $adjacent->getID()) { $adjacent->setSubpriority($shift_target); } // If we're shifting the original target task, update the base // subpriority. if ($shift_task->getID() == $dst->getID()) { $base = $shift_target; } } } $sub = ($adjacent->getSubpriority() + $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); } } diff --git a/src/applications/maniphest/event/ManiphestHovercardEventListener.php b/src/applications/maniphest/event/ManiphestHovercardEventListener.php index a33a854bc6..5615b78d04 100644 --- a/src/applications/maniphest/event/ManiphestHovercardEventListener.php +++ b/src/applications/maniphest/event/ManiphestHovercardEventListener.php @@ -1,90 +1,93 @@ listen(PhabricatorEventType::TYPE_UI_DIDRENDERHOVERCARD); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_DIDRENDERHOVERCARD: $this->handleHovercardEvent($event); break; } } private function handleHovercardEvent(PhutilEvent $event) { $viewer = $event->getUser(); $hovercard = $event->getValue('hovercard'); $handle = $event->getValue('handle'); $phid = $handle->getPHID(); $task = $event->getValue('object'); if (!($task instanceof ManiphestTask)) { return; } $e_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; // Fun with "Unbeta Pholio", hua hua $e_dep_on = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $e_dep_by = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes( array( $e_project, $e_dep_on, $e_dep_by, )); $edges = idx($edge_query->execute(), $phid); $edge_phids = $edge_query->getDestinationPHIDs(); $owner_phid = $task->getOwnerPHID(); $hovercard ->setTitle(pht('T%d', $task->getID())) ->setDetail($task->getTitle()); if ($owner_phid) { $owner = $viewer->renderHandle($owner_phid); } else { $owner = phutil_tag('em', array(), pht('None')); } $hovercard->addField(pht('Assigned To'), $owner); + $hovercard->addField( + pht('Priority'), + ManiphestTaskPriority::getTaskPriorityName($task->getPriority())); if ($edge_phids) { $edge_types = array( $e_project => pht('Projects'), - $e_dep_by => pht('Dependent Tasks'), - $e_dep_on => pht('Depends On'), + $e_dep_by => pht('Blocks'), + $e_dep_on => pht('Blocked By'), ); $max_count = 6; foreach ($edge_types as $edge_type => $edge_name) { if ($edges[$edge_type]) { // TODO: This can be made more sophisticated. We still load all // edges into memory. Only load the ones we need. $edge_overflow = array(); if (count($edges[$edge_type]) > $max_count) { $edges[$edge_type] = array_slice($edges[$edge_type], 0, 6, true); $edge_overflow = ', ...'; } $hovercard->addField( $edge_name, array( $viewer->renderHandleList(array_keys($edges[$edge_type])), $edge_overflow, )); } } } $hovercard->addTag(ManiphestView::renderTagForTask($task)); $event->setValue('hovercard', $hovercard); } } diff --git a/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php b/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php index eade548740..d504a71214 100644 --- a/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php +++ b/src/applications/maniphest/herald/HeraldManiphestTaskAdapter.php @@ -1,124 +1,65 @@ task = $this->newObject(); } public function supportsApplicationEmail() { return true; } public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, HeraldRepetitionPolicyConfig::FIRST, ); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } public function setTask(ManiphestTask $task) { $this->task = $task; return $this; } public function getTask() { return $this->task; } public function getObject() { return $this->task; } - public function setAssignPHID($assign_phid) { - $this->assignPHID = $assign_phid; - return $this; - } - public function getAssignPHID() { - return $this->assignPHID; - } - public function getAdapterContentName() { return pht('Maniphest Tasks'); } - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_ASSIGN_TASK, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_FLAG, - self::ACTION_ASSIGN_TASK, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } - } - public function getHeraldName() { return 'T'.$this->getTask()->getID(); } - public function applyHeraldEffects(array $effects) { - assert_instances_of($effects, 'HeraldEffect'); - - $result = array(); - foreach ($effects as $effect) { - $action = $effect->getAction(); - switch ($action) { - case self::ACTION_ASSIGN_TASK: - $target_array = $effect->getTarget(); - $assign_phid = reset($target_array); - $this->setAssignPHID($assign_phid); - $result[] = new HeraldApplyTranscript( - $effect, - true, - pht('Assigned task.')); - break; - default: - $result[] = $this->applyStandardEffect($effect); - break; - } - } - return $result; - } - } diff --git a/src/applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php new file mode 100644 index 0000000000..79e74e7466 --- /dev/null +++ b/src/applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php @@ -0,0 +1,61 @@ +getAdapter(); + $object = $adapter->getObject(); + + $current = array($object->getOwnerPHID()); + + $allowed_types = array( + PhabricatorPeopleUserPHIDType::TYPECONST, + ); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phid = head_key($targets); + + $xaction = $adapter->newTransaction() + ->setTransactionType(ManiphestTransaction::TYPE_OWNER) + ->setNewValue($phid); + + $adapter->queueTransaction($xaction); + + $this->logEffect(self::DO_ASSIGN, array($phid)); + } + + protected function getActionEffectMap() { + return array( + self::DO_ASSIGN => array( + 'icon' => 'fa-user', + 'color' => 'green', + 'name' => pht('Assigned Task'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_ASSIGN: + return pht( + 'Assigned task to: %s.', + $this->renderHandleList($data)); + } + } + +} diff --git a/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php new file mode 100644 index 0000000000..6df37056e3 --- /dev/null +++ b/src/applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php @@ -0,0 +1,34 @@ +applyAssign($effect->getTarget()); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + // TODO: Eventually, it would be nice to get "limit = 1" exported from here + // up to the UI. + return new PhabricatorPeopleDatasource(); + } + + public function renderActionDescription($value) { + return pht('Assign task to: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php b/src/applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php new file mode 100644 index 0000000000..a48bafb53e --- /dev/null +++ b/src/applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php @@ -0,0 +1,29 @@ +getRule()->getAuthorPHID(); + return $this->applyAssign(array($phid)); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Assign task to rule author.'); + } + +} diff --git a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php index b43c728257..ae1c5f8748 100644 --- a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php +++ b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php @@ -1,123 +1,127 @@ application = $data['application']; $this->action = $data['action']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $selected = PhabricatorApplication::getByClass($this->application); + $selected = id(new PhabricatorApplicationQuery()) + ->setViewer($user) + ->withClasses(array($this->application)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$selected) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('view/'.$this->application); $prototypes_enabled = PhabricatorEnv::getEnvConfig( 'phabricator.show-prototypes'); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addCancelButton($view_uri); if ($selected->isPrototype() && !$prototypes_enabled) { $dialog ->setTitle(pht('Prototypes Not Enabled')) ->appendChild( pht( 'To manage prototypes, enable them by setting %s in your '. 'Phabricator configuration.', phutil_tag('tt', array(), 'phabricator.show-prototypes'))); return id(new AphrontDialogResponse())->setDialog($dialog); } if ($request->isDialogFormPost()) { $this->manageApplication(); return id(new AphrontRedirectResponse())->setURI($view_uri); } if ($this->action == 'install') { if ($selected->canUninstall()) { $dialog ->setTitle(pht('Confirmation')) ->appendChild( pht( 'Install %s application?', $selected->getName())) ->addSubmitButton(pht('Install')); } else { $dialog ->setTitle(pht('Information')) ->appendChild(pht('You cannot install an installed application.')); } } else { if ($selected->canUninstall()) { $dialog->setTitle(pht('Really Uninstall Application?')); if ($selected instanceof PhabricatorHomeApplication) { $dialog ->appendParagraph( pht( 'Are you absolutely certain you want to uninstall the Home '. 'application?')) ->appendParagraph( pht( 'This is very unusual and will leave you without any '. 'content on the Phabricator home page. You should only '. 'do this if you are certain you know what you are doing.')) ->addSubmitButton(pht('Completely Break Phabricator')); } else { $dialog ->appendParagraph( pht( 'Really uninstall the %s application?', $selected->getName())) ->addSubmitButton(pht('Uninstall')); } } else { $dialog ->setTitle(pht('Information')) ->appendChild( pht( 'This application cannot be uninstalled, '. 'because it is required for Phabricator to work.')); } } return id(new AphrontDialogResponse())->setDialog($dialog); } public function manageApplication() { $key = 'phabricator.uninstalled-applications'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $list = $config_entry->getValue(); $uninstalled = PhabricatorEnv::getEnvConfig($key); if (isset($uninstalled[$this->application])) { unset($list[$this->application]); } else { $list[$this->application] = true; } PhabricatorConfigEditor::storeNewValue( $this->getRequest()->getUser(), $config_entry, $list, PhabricatorContentSource::newFromRequest($this->getRequest())); } } diff --git a/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php b/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php new file mode 100644 index 0000000000..835bde8d4d --- /dev/null +++ b/src/applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php @@ -0,0 +1,85 @@ +getAdapter(); + + $allowed_types = array( + PhabricatorPeopleUserPHIDType::TYPECONST, + PhabricatorProjectProjectPHIDType::TYPECONST, + ); + + // There's no stateful behavior for this action: we always just send an + // email. + $current = array(); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + foreach ($phids as $phid) { + $adapter->addEmailPHID($phid, $force); + } + + if ($force) { + $this->logEffect(self::DO_FORCE, $phids); + } else { + $this->logEffect(self::DO_SEND, $phids); + } + } + + protected function getActionEffectMap() { + return array( + self::DO_SEND => array( + 'icon' => 'fa-envelope', + 'color' => 'green', + 'name' => pht('Sent Mail'), + ), + self::DO_FORCE => array( + 'icon' => 'fa-envelope', + 'color' => 'blue', + 'name' => pht('Forced Mail'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_SEND: + return pht( + 'Queued email to be delivered to %s target(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_FORCE: + return pht( + 'Queued email to be delivered to %s target(s), ignoring their '. + 'notification preferences: %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + } + } + +} diff --git a/src/applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php b/src/applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php new file mode 100644 index 0000000000..d569848e01 --- /dev/null +++ b/src/applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php @@ -0,0 +1,32 @@ +applyEmail($effect->getTarget(), $force = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorMetaMTAMailableDatasource(); + } + + public function renderActionDescription($value) { + return pht('Send an email to: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php b/src/applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php new file mode 100644 index 0000000000..67563da521 --- /dev/null +++ b/src/applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php @@ -0,0 +1,34 @@ +getRule()->getAuthorPHID(); + + // For personal rules, we'll force delivery of a real email. This effect + // is stronger than notification preferences, so you get an actual email + // even if your preferences are set to "Notify" or "Ignore". + + return $this->applyEmail(array($phid), $force = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Send an email to rule author.'); + } + +} diff --git a/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php new file mode 100644 index 0000000000..c907bf1050 --- /dev/null +++ b/src/applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php @@ -0,0 +1,79 @@ +setName('volume') + ->setSynopsis( + pht('Show how much mail users have received in the last 30 days.')) + ->setExamples( + '**volume**') + ->setArguments( + array( + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + $viewer = $this->getViewer(); + + $since = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); + $until = PhabricatorTime::getNow(); + + $mails = id(new PhabricatorMetaMTAMailQuery()) + ->setViewer($viewer) + ->withDateCreatedBetween($since, $until) + ->execute(); + + $unfiltered = array(); + + foreach ($mails as $mail) { + $unfiltered_actors = mpull($mail->loadAllActors(), 'getPHID'); + foreach ($unfiltered_actors as $phid) { + if (empty($unfiltered[$phid])) { + $unfiltered[$phid] = 0; + } + $unfiltered[$phid]++; + } + } + + arsort($unfiltered); + + $table = id(new PhutilConsoleTable()) + ->setBorders(true) + ->addColumn( + 'user', + array( + 'title' => pht('User'), + )) + ->addColumn( + 'unfiltered', + array( + 'title' => pht('Unfiltered'), + )); + + $handles = $viewer->loadHandles(array_keys($unfiltered)); + $names = mpull(iterator_to_array($handles), 'getName', 'getPHID'); + + foreach ($unfiltered as $phid => $count) { + $table->addRow( + array( + 'user' => idx($names, $phid), + 'unfiltered' => $count, + )); + } + + $table->draw(); + + echo "\n"; + echo pht('Mail sent in the last 30 days.')."\n"; + echo pht( + '"Unfiltered" is raw volume before preferences were applied.')."\n"; + echo "\n"; + + return 0; + } + +} diff --git a/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php index a0965b7133..e0dadb5f13 100644 --- a/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php @@ -1,115 +1,137 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withActorPHIDs(array $phids) { $this->actorPHIDs = $phids; return $this; } public function withRecipientPHIDs(array $phids) { $this->recipientPHIDs = $phids; return $this; } + public function withDateCreatedBetween($min, $max) { + $this->createdMin = $min; + $this->createdMax = $max; + return $this; + } + protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'mail.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'mail.phid IN (%Ls)', $this->phids); } if ($this->actorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'mail.actorPHID IN (%Ls)', $this->actorPHIDs); } if ($this->recipientPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'recipient.dst IN (%Ls)', $this->recipientPHIDs); } if ($this->actorPHIDs === null && $this->recipientPHIDs === null) { $viewer = $this->getViewer(); + if (!$viewer->isOmnipotent()) { + $where[] = qsprintf( + $conn, + 'edge.dst = %s OR actorPHID = %s', + $viewer->getPHID(), + $viewer->getPHID()); + } + } + + if ($this->createdMin !== null) { $where[] = qsprintf( - $conn_r, - 'edge.dst = %s OR actorPHID = %s', - $viewer->getPHID(), - $viewer->getPHID()); + $conn, + 'mail.dateCreated >= %d', + $this->createdMin); } - $where[] = $this->buildPagingClause($conn_r); + if ($this->createdMax !== null) { + $where[] = qsprintf( + $conn, + 'mail.dateCreated <= %d', + $this->createdMax); + } - return $this->formatWhereClause($where); + return $where; } protected function buildJoinClause(AphrontDatabaseConnection $conn) { $joins = array(); if ($this->actorPHIDs === null && $this->recipientPHIDs === null) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T edge ON mail.phid = edge.src AND edge.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST); } if ($this->recipientPHIDs !== null) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T recipient '. 'ON mail.phid = recipient.src AND recipient.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST); } return implode(' ', $joins); } protected function getPrimaryTableAlias() { return 'mail'; } public function newResultObject() { return new PhabricatorMetaMTAMail(); } public function getQueryApplicationClass() { return 'PhabricatorMetaMTAApplication'; } } diff --git a/src/applications/notification/controller/PhabricatorNotificationClearController.php b/src/applications/notification/controller/PhabricatorNotificationClearController.php index ba781234e7..0b6027d379 100644 --- a/src/applications/notification/controller/PhabricatorNotificationClearController.php +++ b/src/applications/notification/controller/PhabricatorNotificationClearController.php @@ -1,54 +1,53 @@ getRequest(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $chrono_key = $request->getStr('chronoKey'); - $user = $request->getUser(); if ($request->isDialogFormPost()) { $table = new PhabricatorFeedStoryNotification(); queryfx( $table->establishConnection('w'), 'UPDATE %T SET hasViewed = 1 '. 'WHERE userPHID = %s AND hasViewed = 0 and chronologicalKey <= %s', $table->getTableName(), - $user->getPHID(), + $viewer->getPHID(), $chrono_key); return id(new AphrontReloadResponse()) ->setURI('/notification/'); } $dialog = new AphrontDialogView(); - $dialog->setUser($user); + $dialog->setUser($viewer); $dialog->addCancelButton('/notification/'); if ($chrono_key) { $dialog->setTitle(pht('Really mark all notifications as read?')); $dialog->addHiddenInput('chronoKey', $chrono_key); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { $dialog->appendChild( pht( 'All unread notifications will be marked as read. You can not '. 'undo this action.')); } else { $dialog->appendChild( pht( "You can't ignore your problems forever, you know.")); } $dialog->addSubmitButton(pht('Mark All Read')); } else { $dialog->setTitle(pht('No notifications to mark as read.')); $dialog->appendChild(pht('You have no unread notifications.')); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php index 6e6f7361df..68214d1965 100644 --- a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php +++ b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php @@ -1,60 +1,59 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $stories = id(new PhabricatorNotificationQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withKeys(array($request->getStr('key'))) ->execute(); if (!$stories) { return $this->buildEmptyResponse(); } $story = head($stories); if ($story->getAuthorPHID() === $viewer->getPHID()) { // Don't show the user individual notifications about their own // actions. Primarily, this stops pages from showing notifications // immediately after you click "Submit" on a comment form if the // notification server returns faster than the web server. // TODO: It would be nice to retain the "page updated" bubble on copies // of the page that are open in other tabs, but there isn't an obvious // way to do this easily. return $this->buildEmptyResponse(); } $builder = new PhabricatorNotificationBuilder(array($story)); $content = $builder->buildView()->render(); $dict = $builder->buildDict(); $data = $dict[0]; $response = array( 'pertinent' => true, 'primaryObjectPHID' => $story->getPrimaryObjectPHID(), 'desktopReady' => $data['desktopReady'], 'href' => $data['href'], 'icon' => $data['icon'], 'title' => $data['title'], 'body' => $data['body'], 'content' => hsprintf('%s', $content), ); return id(new AphrontAjaxResponse())->setContent($response); } private function buildEmptyResponse() { return id(new AphrontAjaxResponse())->setContent( array( 'pertinent' => false, )); } } diff --git a/src/applications/notification/controller/PhabricatorNotificationListController.php b/src/applications/notification/controller/PhabricatorNotificationListController.php index b7ab15df90..718c2431a1 100644 --- a/src/applications/notification/controller/PhabricatorNotificationListController.php +++ b/src/applications/notification/controller/PhabricatorNotificationListController.php @@ -1,35 +1,31 @@ getURIData('queryKey'); - public function willProcessRequest(array $data) { - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhabricatorNotificationSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView() { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhabricatorNotificationSearchEngine()) - ->setViewer($user) + ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } } diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index 3e8f16b07a..c16069a0e6 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -1,93 +1,91 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $query = id(new PhabricatorNotificationQuery()) - ->setViewer($user) - ->withUserPHIDs(array($user->getPHID())) + ->setViewer($viewer) + ->withUserPHIDs(array($viewer->getPHID())) ->setLimit(15); $stories = $query->execute(); $clear_ui_class = 'phabricator-notification-clear-all'; $clear_uri = id(new PhutilURI('/notification/clear/')); if ($stories) { $builder = new PhabricatorNotificationBuilder($stories); $notifications_view = $builder->buildView(); $content = $notifications_view->render(); $clear_uri->setQueryParam( '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')); if (PhabricatorEnv::getEnvConfig('notification.enabled')) { $connection_status = new PhabricatorNotificationStatusView(); } else { $connection_status = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'Notifications User Guide: Setup and Configuration'), ), pht('Notification Server not enabled.')); } $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', $header, $content, $connection_ui); $unread_count = id(new PhabricatorFeedStoryNotification()) - ->countUnread($user); + ->countUnread($viewer); $json = array( 'content' => $content, 'number' => (int)$unread_count, ); return id(new AphrontAjaxResponse())->setContent($json); } } diff --git a/src/applications/notification/controller/PhabricatorNotificationStatusController.php b/src/applications/notification/controller/PhabricatorNotificationStatusController.php index 2c9e308985..e889c7af5e 100644 --- a/src/applications/notification/controller/PhabricatorNotificationStatusController.php +++ b/src/applications/notification/controller/PhabricatorNotificationStatusController.php @@ -1,84 +1,85 @@ renderServerStatus($status); } catch (Exception $ex) { $status = new PHUIInfoView(); $status->setTitle(pht('Notification Server Issue')); $status->appendChild(hsprintf( '%s

'. '%s %s', pht( 'Unable to determine server status. This probably means the server '. 'is not in great shape. The specific issue encountered was:'), get_class($ex), phutil_escape_html_newlines($ex->getMessage()))); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Status')); return $this->buildApplicationPage( array( $crumbs, $status, ), array( 'title' => pht('Notification Server Status'), 'device' => false, )); } private function renderServerStatus(array $status) { $rows = array(); foreach ($status as $key => $value) { switch ($key) { case 'uptime': $value /= 1000; $value = phutil_format_relative_time_detailed($value); break; case 'log': case 'instance': break; default: $value = number_format($value); break; } $rows[] = array($key, $value); } $table = new AphrontTableView($rows); $table->setColumnClasses( array( 'header', 'wide', )); $test_icon = id(new PHUIIconView()) ->setIconFont('fa-exclamation-triangle'); $test_button = id(new PHUIButtonView()) ->setTag('a') ->setWorkflow(true) ->setText(pht('Send Test Notification')) ->setHref($this->getApplicationURI('test/')) ->setIcon($test_icon); $header = id(new PHUIHeaderView()) ->setHeader(pht('Notification Server Status')) ->addActionLink($test_button); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table); return $box; } } diff --git a/src/applications/notification/controller/PhabricatorNotificationTestController.php b/src/applications/notification/controller/PhabricatorNotificationTestController.php index 706242818c..5c76e53357 100644 --- a/src/applications/notification/controller/PhabricatorNotificationTestController.php +++ b/src/applications/notification/controller/PhabricatorNotificationTestController.php @@ -1,43 +1,42 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $story_type = 'PhabricatorNotificationTestFeedStory'; $story_data = array( 'title' => pht( 'This is a test notification, sent at %s.', phabricator_datetime(time(), $viewer)), ); $viewer_phid = $viewer->getPHID(); // NOTE: Because we don't currently show you your own notifications, make // sure this comes from a different PHID. $application_phid = id(new PhabricatorNotificationsApplication()) ->getPHID(); // TODO: When it's easier to get these buttons to render as forms, this // would be slightly nicer as a more standard isFormPost() check. if ($request->validateCSRF()) { id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) ->setStoryTime(time()) ->setStoryAuthorPHID($application_phid) ->setRelatedPHIDs(array($viewer_phid)) ->setPrimaryObjectPHID($viewer_phid) ->setSubscribedPHIDs(array($viewer_phid)) ->setNotifyAuthor(true) ->publish(); } return id(new AphrontAjaxResponse()); } } diff --git a/src/applications/nuance/controller/NuanceItemEditController.php b/src/applications/nuance/controller/NuanceItemEditController.php index b5e02cbda6..89bfbda5c9 100644 --- a/src/applications/nuance/controller/NuanceItemEditController.php +++ b/src/applications/nuance/controller/NuanceItemEditController.php @@ -1,49 +1,32 @@ getViewer(); + $id = $request->getURIData('id'); - public function setItemID($item_id) { - $this->itemID = $item_id; - return $this; - } - public function getItemID() { - return $this->itemID; - } - - public function willProcessRequest(array $data) { - $this->setItemID(idx($data, 'id')); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $item_id = $this->getItemID(); - $is_new = !$item_id; - - if ($is_new) { + if (!$id) { $item = new NuanceItem(); } else { $item = id(new NuanceItemQuery()) - ->setViewer($user) - ->withIDs(array($item_id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); } if (!$item) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $title = 'TODO'; return $this->buildApplicationPage( $crumbs, array( 'title' => $title, )); } } diff --git a/src/applications/nuance/controller/NuanceItemViewController.php b/src/applications/nuance/controller/NuanceItemViewController.php index e0ba457e89..9c7401cafe 100644 --- a/src/applications/nuance/controller/NuanceItemViewController.php +++ b/src/applications/nuance/controller/NuanceItemViewController.php @@ -1,42 +1,27 @@ getViewer(); + $id = $request->getURIData('id'); - public function setItemID($item_id) { - $this->itemID = $item_id; - return $this; - } - public function getItemID() { - return $this->itemID; - } - - public function willProcessRequest(array $data) { - $this->setItemID($data['id']); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $item_id = $this->getItemID(); $item = id(new NuanceItemQuery()) - ->setViewer($user) - ->withIDs(array($item_id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$item) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $title = 'TODO'; return $this->buildApplicationPage( $crumbs, array( 'title' => $title, )); } } diff --git a/src/applications/nuance/controller/NuanceRequestorEditController.php b/src/applications/nuance/controller/NuanceRequestorEditController.php index 42405a227f..2efc49f711 100644 --- a/src/applications/nuance/controller/NuanceRequestorEditController.php +++ b/src/applications/nuance/controller/NuanceRequestorEditController.php @@ -1,50 +1,33 @@ getViewer(); + $id = $request->getURIData('id'); - public function setRequestorID($requestor_id) { - $this->requestorID = $requestor_id; - return $this; - } - public function getRequestorID() { - return $this->requestorID; - } - - public function willProcessRequest(array $data) { - $this->setRequestorID(idx($data, 'id')); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $requestor_id = $this->getRequestorID(); - $is_new = !$requestor_id; - - if ($is_new) { + if (!$id) { $requestor = new NuanceRequestor(); } else { $requestor = id(new NuanceRequestorQuery()) - ->setViewer($user) - ->withIDs(array($requestor_id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); } if (!$requestor) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $title = pht('TODO'); return $this->buildApplicationPage( $crumbs, array( 'title' => $title, )); } } diff --git a/src/applications/nuance/controller/NuanceRequestorViewController.php b/src/applications/nuance/controller/NuanceRequestorViewController.php index 08dcedb62a..8f6912c4d7 100644 --- a/src/applications/nuance/controller/NuanceRequestorViewController.php +++ b/src/applications/nuance/controller/NuanceRequestorViewController.php @@ -1,42 +1,27 @@ getViewer(); + $id = $request->getURIData('id'); - public function setRequestorID($requestor_id) { - $this->requestorID = $requestor_id; - return $this; - } - public function getRequestorID() { - return $this->requestorID; - } - - public function willProcessRequest(array $data) { - $this->setRequestorID($data['id']); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $requestor_id = $this->getRequestorID(); $requestor = id(new NuanceRequestorQuery()) - ->setViewer($user) - ->withIDs(array($requestor_id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$requestor) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $title = 'TODO'; return $this->buildApplicationPage( $crumbs, array( 'title' => $title, )); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialConduitController.php b/src/applications/passphrase/controller/PassphraseCredentialConduitController.php index 4c65b5d07d..b86d18c227 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialConduitController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialConduitController.php @@ -1,81 +1,75 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $view_uri = '/K'.$credential->getID(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $view_uri); $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType(PassphraseCredentialTransaction::TYPE_CONDUIT) ->setNewValue(!$credential->getAllowConduit()); $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } if ($credential->getAllowConduit()) { return $this->newDialog() ->setTitle(pht('Prevent Conduit access?')) ->appendChild( pht( 'This credential and its secret will no longer be able '. 'to be retrieved using the `%s` method in Conduit.', 'passphrase.query')) ->addSubmitButton(pht('Prevent Conduit Access')) ->addCancelButton($view_uri); } else { return $this->newDialog() ->setTitle(pht('Allow Conduit access?')) ->appendChild( pht( 'This credential will be able to be retrieved via the Conduit '. 'API by users who have access to this credential. You should '. 'only enable this for credentials which need to be accessed '. 'programmatically (such as from build agents).')) ->addSubmitButton(pht('Allow Conduit Access')) ->addCancelButton($view_uri); } } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialCreateController.php b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php index a6462b8da8..cfddcbcc4a 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialCreateController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php @@ -1,69 +1,68 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $types = PassphraseCredentialType::getAllCreateableTypes(); $types = mpull($types, null, 'getCredentialType'); $types = msort($types, 'getCredentialTypeName'); $errors = array(); $e_type = null; if ($request->isFormPost()) { $type = $request->getStr('type'); if (empty($types[$type])) { $errors[] = pht('You must choose a credential type.'); $e_type = pht('Required'); } if (!$errors) { $uri = $this->getApplicationURI('edit/?type='.$type); return id(new AphrontRedirectResponse())->setURI($uri); } } $types_control = id(new AphrontFormRadioButtonControl()) ->setName('type') ->setLabel(pht('Credential Type')) ->setError($e_type); foreach ($types as $type) { $types_control->addButton( $type->getCredentialType(), $type->getCredentialTypeName(), $type->getCredentialTypeDescription()); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($types_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue')) ->addCancelButton($this->getApplicationURI())); $title = pht('New Credential'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Create')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Credential')) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php index 53f9e0650f..8858d0ef6b 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php @@ -1,65 +1,59 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } $view_uri = '/K'.$credential->getID(); if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType(PassphraseCredentialTransaction::TYPE_DESTROY) ->setNewValue(1); $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } return $this->newDialog() ->setUser($viewer) ->setTitle(pht('Really destroy credential?')) ->appendChild( pht( 'This credential will be deactivated and the secret will be '. 'unrecoverably destroyed. Anything relying on this credential '. 'will cease to function. This operation can not be undone.')) ->addSubmitButton(pht('Destroy Credential')) ->addCancelButton($view_uri); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 36033c7c01..8a2a8da70f 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -1,383 +1,377 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { + if ($id) { $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = $this->getCredentialType($credential->getCredentialType()); $is_new = false; } else { $type_const = $request->getStr('type'); $type = $this->getCredentialType($type_const); if (!$type->isCreateable()) { throw new Exception( pht( 'Credential has noncreateable type "%s"!', $credential->getCredentialType())); } $credential = PassphraseCredential::initializeNewCredential($viewer) ->setCredentialType($type->getCredentialType()) ->setProvidesType($type->getProvidesType()); $is_new = true; // Prefill username if provided. $credential->setUsername((string)$request->getStr('username')); if (!$request->getStr('isInitialized')) { $type->didInitializeNewCredential($viewer, $credential); } } $errors = array(); $v_name = $credential->getName(); $e_name = true; $v_desc = $credential->getDescription(); $v_space = $credential->getSpacePHID(); $v_username = $credential->getUsername(); $e_username = true; $v_is_locked = false; $bullet = "\xE2\x80\xA2"; $v_secret = $credential->getSecretID() ? str_repeat($bullet, 32) : null; if ($is_new && ($v_secret === null)) { // If we're creating a new credential, the credential type may have // populated the secret for us (for example, generated an SSH key). In // this case, try { $v_secret = $credential->getSecret()->openEnvelope(); } catch (Exception $ex) { // Ignore this. } } $validation_exception = null; $errors = array(); $e_password = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_username = $request->getStr('username'); $v_view_policy = $request->getStr('viewPolicy'); $v_edit_policy = $request->getStr('editPolicy'); $v_is_locked = $request->getStr('lock'); $v_secret = $request->getStr('secret'); $v_space = $request->getStr('spacePHID'); $v_password = $request->getStr('password'); $v_decrypt = $v_secret; $env_secret = new PhutilOpaqueEnvelope($v_secret); $env_password = new PhutilOpaqueEnvelope($v_password); if ($type->requiresPassword($env_secret)) { if (strlen($v_password)) { $v_decrypt = $type->decryptSecret($env_secret, $env_password); if ($v_decrypt === null) { $e_password = pht('Incorrect'); $errors[] = pht( 'This key requires a password, but the password you provided '. 'is incorrect.'); } else { $v_decrypt = $v_decrypt->openEnvelope(); } } else { $e_password = pht('Required'); $errors[] = pht( 'This key requires a password. You must provide the password '. 'for the key.'); } } if (!$errors) { $type_name = PassphraseCredentialTransaction::TYPE_NAME; $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION; $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; $type_is_locked = PassphraseCredentialTransaction::TYPE_LOCK; $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_space = PhabricatorTransactions::TYPE_SPACE; $xactions = array(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_desc) ->setNewValue($v_desc); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_view_policy) ->setNewValue($v_view_policy); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_edit_policy) ->setNewValue($v_edit_policy); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_space) ->setNewValue($v_space); // Open a transaction in case we're writing a new secret; this limits // the amount of code which handles secret plaintexts. $credential->openTransaction(); if (!$credential->getIsLocked()) { if ($type->shouldRequireUsername()) { $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_username) ->setNewValue($v_username); } // If some value other than a sequence of bullets was provided for // the credential, update it. In particular, note that we are // explicitly allowing empty secrets: one use case is HTTP auth where // the username is a secret token which covers both identity and // authentication. if (!preg_match('/^('.$bullet.')+$/', trim($v_decrypt))) { // If the credential was previously destroyed, restore it when it is // edited if a secret is provided. $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_destroy) ->setNewValue(0); $new_secret = id(new PassphraseSecret()) ->setSecretData($v_decrypt) ->save(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_secret_id) ->setNewValue($new_secret->getID()); } $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_is_locked) ->setNewValue($v_is_locked); } try { $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); $credential->saveTransaction(); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent( array( 'phid' => $credential->getPHID(), 'name' => 'K'.$credential->getID().' '.$credential->getName(), )); } else { return id(new AphrontRedirectResponse()) ->setURI('/K'.$credential->getID()); } } catch (PhabricatorApplicationTransactionValidationException $ex) { $credential->killTransaction(); $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); $e_username = $ex->getShortMessage($type_username); $credential->setViewPolicy($v_view_policy); $credential->setEditPolicy($v_edit_policy); } } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($credential) ->execute(); $secret_control = $type->newSecretControl(); $credential_is_locked = $credential->getIsLocked(); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('isInitialized', true) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Credential Type')) ->setValue($type->getCredentialTypeName())) ->appendChild( id(new AphrontFormDividerControl())) ->appendControl( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($credential) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendControl( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($credential) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormDividerControl())); if ($credential_is_locked) { $form->appendRemarkupInstructions( pht('This credential is permanently locked and can not be edited.')); } if ($type->shouldRequireUsername()) { $form ->appendChild( id(new AphrontFormTextControl()) ->setName('username') ->setLabel(pht('Login/Username')) ->setValue($v_username) ->setDisabled($credential_is_locked) ->setError($e_username)); } $form ->appendChild( $secret_control ->setName('secret') ->setLabel($type->getSecretLabel()) ->setDisabled($credential_is_locked) ->setValue($v_secret)); if ($type->shouldShowPasswordField()) { $form->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setName('password') ->setLabel($type->getPasswordLabel()) ->setDisabled($credential_is_locked) ->setError($e_password)); } if ($is_new) { $form->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'lock', 1, array( phutil_tag('strong', array(), pht('Lock Permanently:')), ' ', pht('Prevent the secret from being revealed or changed.'), ), $v_is_locked) ->setDisabled($credential_is_locked)); } $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $title = pht('Create Credential'); $header = pht('Create New Credential'); $crumbs->addTextCrumb(pht('Create')); $cancel_uri = $this->getApplicationURI(); } else { $title = pht('Edit Credential'); $header = pht('Edit Credential %s', 'K'.$credential->getID()); $crumbs->addTextCrumb( 'K'.$credential->getID(), '/K'.$credential->getID()); $crumbs->addTextCrumb(pht('Edit')); $cancel_uri = '/K'.$credential->getID(); } if ($request->isAjax()) { if ($errors) { $errors = id(new PHUIInfoView())->setErrors($errors); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle($title) ->appendChild($errors) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Create Credential')) ->addCancelButton($cancel_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($cancel_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setFormErrors($errors) ->setValidationException($validation_exception) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } private function getCredentialType($type_const) { $type = PassphraseCredentialType::getTypeByConstant($type_const); if (!$type) { throw new Exception( pht('Credential has invalid type "%s"!', $type_const)); } return $type; } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialListController.php b/src/applications/passphrase/controller/PassphraseCredentialListController.php index db36a4a6ba..c4da29a984 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialListController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialListController.php @@ -1,24 +1,20 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PassphraseCredentialSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialLockController.php b/src/applications/passphrase/controller/PassphraseCredentialLockController.php index 3abe4c8820..4a872d8667 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialLockController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialLockController.php @@ -1,74 +1,68 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } $view_uri = '/K'.$credential->getID(); if ($credential->getIsLocked()) { return $this->newDialog() ->setTitle(pht('Credential Already Locked')) ->appendChild( pht( 'This credential has been locked and the secret is '. 'hidden forever. Anything relying on this credential will '. 'still function. This operation can not be undone.')) ->addCancelButton($view_uri, pht('Close')); } if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType(PassphraseCredentialTransaction::TYPE_LOCK) ->setNewValue(1); $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } return $this->newDialog() ->setTitle(pht('Really lock credential?')) ->appendChild( pht( 'This credential will be locked and the secret will be '. 'hidden forever. Anything relying on this credential will '. 'still function. This operation can not be undone.')) ->addSubmitButton(pht('Lock Credential')) ->addCancelButton($view_uri); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialPublicController.php b/src/applications/passphrase/controller/PassphraseCredentialPublicController.php index dc129a327d..56fc6ac4ae 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialPublicController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialPublicController.php @@ -1,59 +1,53 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } if (!$type->hasPublicKey()) { throw new Exception(pht('Credential has no public key!')); } $view_uri = '/'.$credential->getMonogram(); $public_key = $type->getPublicKey($viewer, $credential); $body = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Public Key')) ->setReadOnly(true) ->setValue($public_key)); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Public Key (%s)', $credential->getMonogram())) ->appendChild($body) ->addCancelButton($view_uri, pht('Done')); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php index e82dea7408..c61086b0e7 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -1,111 +1,105 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->needSecrets(true) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $view_uri = '/K'.$credential->getID(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $view_uri); $is_locked = $credential->getIsLocked(); if ($is_locked) { return $this->newDialog() ->setUser($viewer) ->setTitle(pht('Credential is locked')) ->appendChild( pht( 'This credential can not be shown, because it is locked.')) ->addCancelButton($view_uri); } if ($request->isFormPost()) { $secret = $credential->getSecret(); if (!$secret) { $body = pht('This credential has no associated secret.'); } else if (!strlen($secret->openEnvelope())) { $body = pht('This credential has an empty secret.'); } else { $body = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Plaintext')) ->setReadOnly(true) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($secret->openEnvelope())); } // NOTE: Disable workflow on the cancel button to reload the page so // the viewer can see that their view was logged. $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Credential Secret (%s)', $credential->getMonogram())) ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri, pht('Done')); $type_secret = PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET; $xactions = array(id(new PassphraseCredentialTransaction()) ->setTransactionType($type_secret) ->setNewValue(true), ); $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); return id(new AphrontDialogResponse())->setDialog($dialog); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { $body = pht( 'The secret associated with this credential will be shown in plain '. 'text on your screen.'); } else { $body = pht( 'The secret associated with this credential will be shown in plain '. 'text on your screen. Before continuing, wrap your arms around '. 'your monitor to create a human shield, keeping it safe from '. 'prying eyes. Protect company secrets!'); } return $this->newDialog() ->setUser($viewer) ->setTitle(pht('Really show secret?')) ->appendChild($body) ->addSubmitButton(pht('Show Secret')) ->addCancelButton($view_uri); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php index ede3bab78f..46fd0c4c7a 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialViewController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -1,218 +1,214 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } $timeline = $this->buildTransactionTimeline( $credential, new PassphraseCredentialTransactionQuery()); $timeline->setShouldTerminate(true); $title = pht('%s %s', 'K'.$credential->getID(), $credential->getName()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('K'.$credential->getID()); $header = $this->buildHeaderView($credential); $actions = $this->buildActionView($credential, $type); $properties = $this->buildPropertyView($credential, $type, $actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, ), array( 'title' => $title, )); } private function buildHeaderView(PassphraseCredential $credential) { $viewer = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($credential->getName()) ->setPolicyObject($credential); if ($credential->getIsDestroyed()) { $header->setStatus('fa-ban', 'red', pht('Destroyed')); } return $header; } private function buildActionView( PassphraseCredential $credential, PassphraseCredentialType $type) { $viewer = $this->getRequest()->getUser(); $id = $credential->getID(); $is_locked = $credential->getIsLocked(); if ($is_locked) { $credential_lock_text = pht('Locked Permanently'); $credential_lock_icon = 'fa-lock'; } else { $credential_lock_text = pht('Lock Permanently'); $credential_lock_icon = 'fa-unlock'; } $allow_conduit = $credential->getAllowConduit(); if ($allow_conduit) { $credential_conduit_text = pht('Prevent Conduit Access'); $credential_conduit_icon = 'fa-ban'; } else { $credential_conduit_text = pht('Allow Conduit Access'); $credential_conduit_icon = 'fa-wrench'; } $actions = id(new PhabricatorActionListView()) ->setObjectURI('/K'.$id) ->setObject($credential) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $credential, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Credential')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if (!$credential->getIsDestroyed()) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Destroy Credential')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("destroy/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Secret')) ->setIcon('fa-eye') ->setHref($this->getApplicationURI("reveal/{$id}/")) ->setDisabled(!$can_edit || $is_locked) ->setWorkflow(true)); if ($type->hasPublicKey()) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Public Key')) ->setIcon('fa-download') ->setHref($this->getApplicationURI("public/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } $actions->addAction( id(new PhabricatorActionView()) ->setName($credential_conduit_text) ->setIcon($credential_conduit_icon) ->setHref($this->getApplicationURI("conduit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setName($credential_lock_text) ->setIcon($credential_lock_icon) ->setHref($this->getApplicationURI("lock/{$id}/")) ->setDisabled(!$can_edit || $is_locked) ->setWorkflow(true)); } return $actions; } private function buildPropertyView( PassphraseCredential $credential, PassphraseCredentialType $type, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($credential) ->setActionList($actions); $properties->addProperty( pht('Credential Type'), $type->getCredentialTypeName()); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $credential); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); if ($type->shouldRequireUsername()) { $properties->addProperty( pht('Username'), $credential->getUsername()); } $used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $credential->getPHID(), PhabricatorCredentialsUsedByObjectEdgeType::EDGECONST); if ($used_by_phids) { $properties->addProperty( pht('Used By'), $viewer->renderHandleList($used_by_phids)); } + $properties->invokeWillRenderEvent(); + $description = $credential->getDescription(); if (strlen($description)) { $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent($description), 'default', $viewer)); } return $properties; } } diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index 3ad1351061..1c58ced82d 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -1,174 +1,192 @@ setViewer($actor) ->withClasses(array('PhabricatorPassphraseApplication')) ->executeOne(); $view_policy = $app->getPolicy(PassphraseDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PassphraseDefaultEditCapability::CAPABILITY); return id(new PassphraseCredential()) ->setName('') ->setUsername('') ->setDescription('') ->setIsDestroyed(0) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } public function getMonogram() { return 'K'.$this->getID(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'credentialType' => 'text64', 'providesType' => 'text64', 'description' => 'text', 'username' => 'text255', 'secretID' => 'id?', 'isDestroyed' => 'bool', 'isLocked' => 'bool', 'allowConduit' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_secret' => array( 'columns' => array('secretID'), 'unique' => true, ), 'key_type' => array( 'columns' => array('credentialType'), ), 'key_provides' => array( 'columns' => array('providesType'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PassphraseCredentialPHIDType::TYPECONST); } public function attachSecret(PhutilOpaqueEnvelope $secret = null) { $this->secret = $secret; return $this; } public function getSecret() { return $this->assertAttached($this->secret); } public function getCredentialTypeImplementation() { $type = $this->getCredentialType(); return PassphraseCredentialType::getTypeByConstant($type); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PassphraseCredentialTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PassphraseCredentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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) { return false; } public function describeAutomaticCapability($capability) { return null; } + +/* -( PhabricatorSubscribableInterface )----------------------------------- */ + + + public function isAutomaticallySubscribed($phid) { + return false; + } + + public function shouldShowSubscribersProperty() { + return true; + } + + public function shouldAllowSubscription($phid) { + return true; + } + + /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $secrets = id(new PassphraseSecret())->loadAllWhere( 'id = %d', $this->getSecretID()); foreach ($secrets as $secret) { $secret->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/paste/controller/PhabricatorPasteCommentController.php b/src/applications/paste/controller/PhabricatorPasteCommentController.php index cd35c4a36e..06596e5be7 100644 --- a/src/applications/paste/controller/PhabricatorPasteCommentController.php +++ b/src/applications/paste/controller/PhabricatorPasteCommentController.php @@ -1,69 +1,63 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $paste = id(new PhabricatorPasteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$paste) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $view_uri = $paste->getURI(); $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorPasteTransactionComment()) ->setContent($request->getStr('comment'))); $editor = id(new PhabricatorPasteEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($paste, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } } diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php index dcc4900951..19b394c18a 100644 --- a/src/applications/paste/controller/PhabricatorPasteEditController.php +++ b/src/applications/paste/controller/PhabricatorPasteEditController.php @@ -1,244 +1,250 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $parent = null; $parent_id = null; - if (!$this->id) { + if (!$id) { $is_create = true; - $paste = PhabricatorPaste::initializeNewPaste($user); + $paste = PhabricatorPaste::initializeNewPaste($viewer); $parent_id = $request->getStr('parent'); if ($parent_id) { // NOTE: If the Paste is forked from a paste which the user no longer // has permission to see, we still let them edit it. $parent = id(new PhabricatorPasteQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($parent_id)) ->needContent(true) ->needRawContent(true) ->execute(); $parent = head($parent); if ($parent) { $paste->setParentPHID($parent->getPHID()); $paste->setViewPolicy($parent->getViewPolicy()); } } - $paste->setAuthorPHID($user->getPHID()); + $paste->setAuthorPHID($viewer->getPHID()); $paste->attachRawContent(''); } else { $is_create = false; $paste = id(new PhabricatorPasteQuery()) - ->setViewer($user) + ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needRawContent(true) ->executeOne(); if (!$paste) { return new Aphront404Response(); } } $v_space = $paste->getSpacePHID(); if ($is_create && $parent) { $v_title = pht('Fork of %s', $parent->getFullName()); $v_language = $parent->getLanguage(); $v_text = $parent->getRawContent(); $v_space = $parent->getSpacePHID(); } else { $v_title = $paste->getTitle(); $v_language = $paste->getLanguage(); $v_text = $paste->getRawContent(); } $v_view_policy = $paste->getViewPolicy(); $v_edit_policy = $paste->getEditPolicy(); + $v_status = $paste->getStatus(); if ($is_create) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $paste->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $validation_exception = null; if ($request->isFormPost()) { $xactions = array(); $v_text = $request->getStr('text'); $v_title = $request->getStr('title'); $v_language = $request->getStr('language'); $v_view_policy = $request->getStr('can_view'); $v_edit_policy = $request->getStr('can_edit'); $v_projects = $request->getArr('projects'); $v_space = $request->getStr('spacePHID'); + $v_status = $request->getStr('status'); // NOTE: The author is the only editor and can always view the paste, // so it's impossible for them to choose an invalid policy. if ($is_create || ($v_text !== $paste->getRawContent())) { $file = PhabricatorPasteEditor::initializeFileForPaste( - $user, + $viewer, $v_title, $v_text); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) ->setNewValue($file->getPHID()); } $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) ->setNewValue($v_title); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) ->setNewValue($v_language); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view_policy); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($v_edit_policy); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) ->setNewValue($v_space); + $xactions[] = id(new PhabricatorPasteTransaction()) + ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS) + ->setNewValue($v_status); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PhabricatorPasteEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $xactions = $editor->applyTransactions($paste, $xactions); return id(new AphrontRedirectResponse())->setURI($paste->getURI()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } $form = new AphrontFormView(); $langs = array( '' => pht('(Detect From Filename in Title)'), ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); $form - ->setUser($user) + ->setUser($viewer) ->addHiddenInput('parent', $parent_id) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($v_title) ->setName('title')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Language')) ->setName('language') ->setValue($v_language) ->setOptions($langs)); $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($paste) ->execute(); $form->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($paste) ->setPolicies($policies) ->setValue($v_view_policy) ->setSpacePHID($v_space) ->setName('can_view')); $form->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($paste) ->setPolicies($policies) ->setValue($v_edit_policy) ->setName('can_edit')); + $form->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Status')) + ->setName('status') + ->setValue($v_status) + ->setOptions($paste->getStatusNameMap())); + $form->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())); $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Text')) ->setValue($v_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setCustomClass('PhabricatorMonospaced') ->setName('text')); $submit = new AphrontFormSubmitControl(); if (!$is_create) { $submit->addCancelButton($paste->getURI()); $submit->setValue(pht('Save Paste')); $title = pht('Edit %s', $paste->getFullName()); $short = pht('Edit'); } else { $submit->setValue(pht('Create Paste')); $title = pht('Create New Paste'); $short = pht('Create'); } $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); if ($validation_exception) { $form_box->setValidationException($validation_exception); } $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); if (!$is_create) { $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); } $crumbs->addTextCrumb($short); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/paste/controller/PhabricatorPasteListController.php b/src/applications/paste/controller/PhabricatorPasteListController.php index 3d8cf7fc11..2092443ffc 100644 --- a/src/applications/paste/controller/PhabricatorPasteListController.php +++ b/src/applications/paste/controller/PhabricatorPasteListController.php @@ -1,25 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhabricatorPasteSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index 75f2532358..0273636d00 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -1,204 +1,214 @@ id = $data['id']; $raw_lines = idx($data, 'lines'); $map = array(); if ($raw_lines) { $lines = explode('-', $raw_lines); $first = idx($lines, 0, 0); $last = idx($lines, 1); if ($last) { $min = min($first, $last); $max = max($first, $last); $map = array_fuse(range($min, $max)); } else { $map[$first] = $first; } } $this->highlightMap = $map; } - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $paste = id(new PhabricatorPasteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needContent(true) ->executeOne(); if (!$paste) { return new Aphront404Response(); } $file = id(new PhabricatorFileQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($paste->getFilePHID())) ->executeOne(); if (!$file) { return new Aphront400Response(); } $forks = id(new PhabricatorPasteQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withParentPHIDs(array($paste->getPHID())) ->execute(); $fork_phids = mpull($forks, 'getPHID'); $header = $this->buildHeaderView($paste); - $actions = $this->buildActionView($user, $paste, $file); + $actions = $this->buildActionView($viewer, $paste, $file); $properties = $this->buildPropertyView($paste, $fork_phids, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $source_code = $this->buildSourceCodeView( $paste, null, $this->highlightMap); $source_code = id(new PHUIBoxView()) ->appendChild($source_code) ->addMargin(PHUI::MARGIN_LARGE_LEFT) ->addMargin(PHUI::MARGIN_LARGE_RIGHT) ->addMargin(PHUI::MARGIN_LARGE_TOP); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) ->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); $timeline = $this->buildTransactionTimeline( $paste, new PhabricatorPasteTransactionQuery()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Eat Paste'); - $draft = PhabricatorDraft::newFromUserAndKey($user, $paste->getPHID()); + $draft = PhabricatorDraft::newFromUserAndKey($viewer, $paste->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setObjectPHID($paste->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$paste->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); return $this->buildApplicationPage( array( $crumbs, $object_box, $source_code, $timeline, $add_comment_form, ), array( 'title' => $paste->getFullName(), 'pageObjects' => array($paste->getPHID()), )); } private function buildHeaderView(PhabricatorPaste $paste) { $title = (nonempty($paste->getTitle())) ? $paste->getTitle() : pht('(An Untitled Masterwork)'); + + if ($paste->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()) ->setHeader($title) ->setUser($this->getRequest()->getUser()) + ->setStatus($header_icon, $header_color, $header_name) ->setPolicyObject($paste); return $header; } private function buildActionView( - PhabricatorUser $user, + PhabricatorUser $viewer, PhabricatorPaste $paste, PhabricatorFile $file) { $can_edit = PhabricatorPolicyFilter::hasCapability( - $user, + $viewer, $paste, PhabricatorPolicyCapability::CAN_EDIT); - $can_fork = $user->isLoggedIn(); + $can_fork = $viewer->isLoggedIn(); $fork_uri = $this->getApplicationURI('/create/?parent='.$paste->getID()); return id(new PhabricatorActionListView()) - ->setUser($user) + ->setUser($viewer) ->setObject($paste) ->setObjectURI($this->getRequest()->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Paste')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($this->getApplicationURI('/edit/'.$paste->getID().'/'))) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Fork This Paste')) ->setIcon('fa-code-fork') ->setDisabled(!$can_fork) ->setWorkflow(!$can_fork) ->setHref($fork_uri)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('View Raw File')) ->setIcon('fa-file-text-o') ->setHref($file->getBestURI())); } private function buildPropertyView( PhabricatorPaste $paste, array $child_phids, PhabricatorActionListView $actions) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($paste) ->setActionList($actions); $properties->addProperty( pht('Author'), $viewer->renderHandle($paste->getAuthorPHID())); $properties->addProperty( pht('Created'), phabricator_datetime($paste->getDateCreated(), $viewer)); if ($paste->getParentPHID()) { $properties->addProperty( pht('Forked From'), $viewer->renderHandle($paste->getParentPHID())); } if ($child_phids) { $properties->addProperty( pht('Forks'), $viewer->renderHandleList($child_phids)); } $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $paste); return $properties; } } diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php index 732db28bd7..bae2003afe 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditor.php +++ b/src/applications/paste/editor/PhabricatorPasteEditor.php @@ -1,187 +1,195 @@ $name, 'mime-type' => 'text/plain; charset=utf-8', 'authorPHID' => $actor->getPHID(), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 'editPolicy' => PhabricatorPolicies::POLICY_NOONE, )); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorPasteTransaction::TYPE_CONTENT; $types[] = PhabricatorPasteTransaction::TYPE_TITLE; $types[] = PhabricatorPasteTransaction::TYPE_LANGUAGE; + $types[] = PhabricatorPasteTransaction::TYPE_STATUS; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_COMMENT; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorPasteTransaction::TYPE_CONTENT: return $object->getFilePHID(); case PhabricatorPasteTransaction::TYPE_TITLE: return $object->getTitle(); case PhabricatorPasteTransaction::TYPE_LANGUAGE: return $object->getLanguage(); + case PhabricatorPasteTransaction::TYPE_STATUS: + return $object->getStatus(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorPasteTransaction::TYPE_CONTENT: case PhabricatorPasteTransaction::TYPE_TITLE: case PhabricatorPasteTransaction::TYPE_LANGUAGE: + case PhabricatorPasteTransaction::TYPE_STATUS: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorPasteTransaction::TYPE_CONTENT: $object->setFilePHID($xaction->getNewValue()); return; case PhabricatorPasteTransaction::TYPE_TITLE: $object->setTitle($xaction->getNewValue()); return; case PhabricatorPasteTransaction::TYPE_LANGUAGE: $object->setLanguage($xaction->getNewValue()); return; + case PhabricatorPasteTransaction::TYPE_STATUS: + $object->setStatus($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorPasteTransaction::TYPE_CONTENT: case PhabricatorPasteTransaction::TYPE_TITLE: case PhabricatorPasteTransaction::TYPE_LANGUAGE: + case PhabricatorPasteTransaction::TYPE_STATUS: return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorPasteTransaction::TYPE_CONTENT: return array($xaction->getNewValue()); } return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorPasteTransaction::TYPE_CONTENT: return false; default: break; } } return true; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getAuthorPHID(), $this->getActingAsPHID(), ); } public function getMailTagsMap() { return array( PhabricatorPasteTransaction::MAILTAG_CONTENT => pht('Paste title, language or text changes.'), PhabricatorPasteTransaction::MAILTAG_COMMENT => pht('Someone comments on a paste.'), PhabricatorPasteTransaction::MAILTAG_OTHER => pht('Other paste activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PasteReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("P{$id}: {$name}") ->addHeader('Thread-Topic', "P{$id}"); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $body->addLinkSection( pht('PASTE DETAIL'), PhabricatorEnv::getProductionURI('/P'.$object->getID())); return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return false; } } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index 661701ba09..0fab56b724 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -1,224 +1,241 @@ setViewer($actor) ->withClasses(array('PhabricatorPasteApplication')) ->executeOne(); $view_policy = $app->getPolicy(PasteDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PasteDefaultEditCapability::CAPABILITY); return id(new PhabricatorPaste()) ->setTitle('') + ->setStatus(self::STATUS_ACTIVE) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } + public static function getStatusNameMap() { + return array( + self::STATUS_ACTIVE => pht('Active'), + self::STATUS_ARCHIVED => pht('Archived'), + ); + } + public function getURI() { return '/'.$this->getMonogram(); } public function getMonogram() { return 'P'.$this->getID(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( + 'status' => 'text32', 'title' => 'text255', 'language' => 'text64', 'mailKey' => 'bytes20', 'parentPHID' => 'phid?', // T6203/NULLABILITY // Pastes should always have a view policy. 'viewPolicy' => 'policy?', ), self::CONFIG_KEY_SCHEMA => array( 'parentPHID' => array( 'columns' => array('parentPHID'), ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_language' => array( 'columns' => array('language'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPastePastePHIDType::TYPECONST); } + public function isArchived() { + return ($this->getStatus() == self::STATUS_ARCHIVED); + } + public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getFullName() { $title = $this->getTitle(); if (!$title) { $title = pht('(An Untitled Masterwork)'); } return 'P'.$this->getID().' '.$title; } public function getContent() { return $this->assertAttached($this->content); } public function attachContent($content) { $this->content = $content; return $this; } public function getRawContent() { return $this->assertAttached($this->rawContent); } public function attachRawContent($raw_content) { $this->rawContent = $raw_content; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return $this->viewPolicy; } else if ($capability == PhabricatorPolicyCapability::CAN_EDIT) { return $this->editPolicy; } return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return ($user->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a paste can always view and edit it.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { if ($this->filePHID) { $file = id(new PhabricatorFileQuery()) ->setViewer($engine->getViewer()) ->withPHIDs(array($this->filePHID)) ->executeOne(); if ($file) { $engine->destroyObject($file); } } $this->delete(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorPasteEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorPasteTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php index 34cc6bd92d..d6b962298b 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransaction.php +++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php @@ -1,207 +1,252 @@ getTransactionType()) { case self::TYPE_CONTENT: $phids[] = $this->getObjectPHID(); break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_LANGUAGE: return ($old === null); } return parent::shouldHide(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return 'fa-plus'; break; case self::TYPE_TITLE: case self::TYPE_LANGUAGE: return 'fa-pencil'; break; + case self::TYPE_STATUS: + $new = $this->getNewValue(); + switch ($new) { + case PhabricatorPaste::STATUS_ACTIVE: + return 'fa-check'; + case PhabricatorPaste::STATUS_ARCHIVED: + return 'fa-ban'; + } + break; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_CONTENT: if ($old === null) { return pht( '%s created this paste.', $this->renderHandleLink($author_phid)); } else { return pht( '%s edited the content of this paste.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_TITLE: return pht( '%s updated the paste\'s title to "%s".', $this->renderHandleLink($author_phid), $new); break; case self::TYPE_LANGUAGE: return pht( "%s updated the paste's language.", $this->renderHandleLink($author_phid)); break; + case self::TYPE_STATUS: + switch ($new) { + case PhabricatorPaste::STATUS_ACTIVE: + return pht( + '%s activated this paste.', + $this->renderHandleLink($author_phid)); + case PhabricatorPaste::STATUS_ARCHIVED: + return pht( + '%s archived this paste.', + $this->renderHandleLink($author_phid)); + } + break; + } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_CONTENT: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s edited %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; case self::TYPE_TITLE: return pht( '%s updated the title for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case self::TYPE_LANGUAGE: return pht( '%s updated the language for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; + case self::TYPE_STATUS: + switch ($new) { + case PhabricatorPaste::STATUS_ACTIVE: + return pht( + '%s activated %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + case PhabricatorPaste::STATUS_ARCHIVED: + return pht( + '%s archived %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + break; } return parent::getTitleForFeed(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return PhabricatorTransactions::COLOR_GREEN; + case self::TYPE_STATUS: + switch ($new) { + case PhabricatorPaste::STATUS_ACTIVE: + return 'green'; + case PhabricatorPaste::STATUS_ARCHIVED: + return 'indigo'; + } + break; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $old = $this->getOldValue(); $new = $this->getNewValue(); $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array_filter(array($old, $new))) ->execute(); $files = mpull($files, null, 'getPHID'); $old_text = ''; if (idx($files, $old)) { $old_text = $files[$old]->loadFileData(); } $new_text = ''; if (idx($files, $new)) { $new_text = $files[$new]->loadFileData(); } return $this->renderTextCorpusChangeDetails( $viewer, $old_text, $new_text); } return parent::renderChangeDetails($viewer); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: case self::TYPE_LANGUAGE: $tags[] = self::MAILTAG_CONTENT; break; case PhabricatorTransactions::TYPE_COMMENT: $tags[] = self::MAILTAG_COMMENT; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } } diff --git a/src/applications/phame/controller/PhameController.php b/src/applications/phame/controller/PhameController.php index adf72c0e7d..fba717e8d9 100644 --- a/src/applications/phame/controller/PhameController.php +++ b/src/applications/phame/controller/PhameController.php @@ -1,136 +1,132 @@ getApplicationURI()); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI($base_uri); - $nav->addLabel(pht('Create')); - $nav->addFilter('post/new', pht('New Post')); - $nav->addFilter('blog/new', pht('New Blog')); - $nav->addLabel(pht('Posts')); + $nav->addFilter('post/all', pht('Latest Posts')); $nav->addFilter('post/draft', pht('My Drafts')); $nav->addFilter('post', pht('My Posts')); - $nav->addFilter('post/all', pht('All Posts')); $nav->addLabel(pht('Blogs')); $nav->addFilter('blog/user', pht('Joinable Blogs')); $nav->addFilter('blog/all', pht('All Blogs')); $nav->selectFilter(null); return $nav; } protected function renderPostList( array $posts, PhabricatorUser $viewer, $nodata) { assert_instances_of($posts, 'PhamePost'); $handle_phids = array(); foreach ($posts as $post) { $handle_phids[] = $post->getBloggerPHID(); if ($post->getBlog()) { $handle_phids[] = $post->getBlog()->getPHID(); } } $handles = $viewer->loadHandles($handle_phids); $stories = array(); foreach ($posts as $post) { $blogger = $handles[$post->getBloggerPHID()]->renderLink(); $blogger_uri = $handles[$post->getBloggerPHID()]->getURI(); $blogger_image = $handles[$post->getBloggerPHID()]->getImageURI(); $blog = null; if ($post->getBlog()) { $blog = $handles[$post->getBlog()->getPHID()]->renderLink(); } $phame_post = ''; if ($post->getBody()) { $phame_post = PhabricatorMarkupEngine::summarize($post->getBody()); } $blog_view = $post->getViewURI(); $phame_title = phutil_tag('a', array('href' => $blog_view), $post->getTitle()); $blogger = phutil_tag('strong', array(), $blogger); if ($post->isDraft()) { $title = pht( '%s drafted a blog post on %s.', $blogger, $blog); $title = phutil_tag('em', array(), $title); } else { $title = pht( '%s wrote a blog post on %s.', $blogger, $blog); } $item = id(new PHUIObjectItemView()) ->setObject($post) ->setHeader($post->getTitle()) ->setHref($this->getApplicationURI('post/view/'.$post->getID().'/')); $story = id(new PHUIFeedStoryView()) ->setTitle($title) ->setImage($blogger_image) ->setImageHref($blogger_uri) ->setAppIcon('fa-star') ->setUser($viewer) ->setPontification($phame_post, $phame_title); if (PhabricatorPolicyFilter::hasCapability( $viewer, $post, PhabricatorPolicyCapability::CAN_EDIT)) { $story->addAction(id(new PHUIIconView()) ->setHref($this->getApplicationURI('post/edit/'.$post->getID().'/')) ->setIconFont('fa-pencil')); } if ($post->getDatePublished()) { $story->setEpoch($post->getDatePublished()); } $stories[] = $story; } if (empty($stories)) { return id(new PHUIBoxView()) ->appendChild($nodata) ->addClass('mlt mlb msr msl'); } return $stories; } public function buildApplicationMenu() { return $this->renderSideNavFilterView()->getMenu(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('New Blog')) ->setHref($this->getApplicationURI('/blog/new')) ->setIcon('fa-plus-square')); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('New Post')) ->setHref($this->getApplicationURI('/post/new')) ->setIcon('fa-pencil')); return $crumbs; } } diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index 3b3d6bd92f..19862fbfbe 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -1,211 +1,207 @@ getUser(); $id = $request->getURIData('id'); if ($id) { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $submit_button = pht('Save Changes'); $page_title = pht('Edit Blog'); $cancel_uri = $this->getApplicationURI('blog/view/'.$blog->getID().'/'); $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $blog->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } else { $blog = PhameBlog::initializeNewBlog($user); $submit_button = pht('Create Blog'); $page_title = pht('Create Blog'); $cancel_uri = $this->getApplicationURI(); $v_projects = array(); } $name = $blog->getName(); $description = $blog->getDescription(); $custom_domain = $blog->getDomain(); $skin = $blog->getSkin(); $can_view = $blog->getViewPolicy(); $can_edit = $blog->getEditPolicy(); $can_join = $blog->getJoinPolicy(); $e_name = true; $e_custom_domain = null; $e_view_policy = null; $validation_exception = null; if ($request->isFormPost()) { $name = $request->getStr('name'); $description = $request->getStr('description'); $custom_domain = nonempty($request->getStr('custom_domain'), null); $skin = $request->getStr('skin'); $can_view = $request->getStr('can_view'); $can_edit = $request->getStr('can_edit'); $can_join = $request->getStr('can_join'); $v_projects = $request->getArr('projects'); $xactions = array( id(new PhameBlogTransaction()) ->setTransactionType(PhameBlogTransaction::TYPE_NAME) ->setNewValue($name), id(new PhameBlogTransaction()) ->setTransactionType(PhameBlogTransaction::TYPE_DESCRIPTION) ->setNewValue($description), id(new PhameBlogTransaction()) ->setTransactionType(PhameBlogTransaction::TYPE_DOMAIN) ->setNewValue($custom_domain), id(new PhameBlogTransaction()) ->setTransactionType(PhameBlogTransaction::TYPE_SKIN) ->setNewValue($skin), id(new PhameBlogTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($can_view), id(new PhameBlogTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($can_edit), id(new PhameBlogTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY) ->setNewValue($can_join), ); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhameBlogTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PhameBlogEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($blog, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('blog/view/'.$blog->getID().'/')); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $validation_exception->getShortMessage( PhameBlogTransaction::TYPE_NAME); $e_custom_domain = $validation_exception->getShortMessage( PhameBlogTransaction::TYPE_DOMAIN); $e_view_policy = $validation_exception->getShortMessage( PhabricatorTransactions::TYPE_VIEW_POLICY); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($blog) ->execute(); $skins = PhameSkinSpecification::loadAllSkinSpecifications(); $skins = mpull($skins, 'getName'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($name) ->setID('blog-name') ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($user) ->setLabel(pht('Description')) ->setName('description') ->setValue($description) ->setID('blog-description') ->setUser($user) ->setDisableMacros(true)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($blog) ->setPolicies($policies) ->setError($e_view_policy) ->setValue($can_view) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($blog) ->setPolicies($policies) ->setValue($can_edit) ->setName('can_edit')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN) ->setPolicyObject($blog) ->setPolicies($policies) ->setValue($can_join) ->setName('can_join')) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Custom Domain')) ->setName('custom_domain') ->setValue($custom_domain) ->setCaption( pht('Must include at least one dot (.), e.g. %s', 'blog.example.com')) ->setError($e_custom_domain)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Skin')) ->setName('skin') ->setValue($skin) ->setOptions($skins)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Blogs'), $this->getApplicationURI('blog/')); $crumbs->addTextCrumb($page_title, $this->getApplicationURI('blog/new')); - $nav = $this->renderSideNavFilterView(); - $nav->selectFilter($id ? null : 'blog/new'); - $nav->appendChild( + return $this->buildApplicationPage( array( $crumbs, $form_box, - )); - - return $this->buildApplicationPage( - $nav, + ), array( 'title' => $page_title, )); } } diff --git a/src/applications/phame/controller/blog/PhameBlogListController.php b/src/applications/phame/controller/blog/PhameBlogListController.php index 16486b95da..9d0253d89c 100644 --- a/src/applications/phame/controller/blog/PhameBlogListController.php +++ b/src/applications/phame/controller/blog/PhameBlogListController.php @@ -1,82 +1,87 @@ getUser(); $nav = $this->renderSideNavFilterView(null); $filter = $request->getURIData('filter'); $filter = $nav->selectFilter('blog/'.$filter, 'blog/user'); $query = id(new PhameBlogQuery()) ->setViewer($user); switch ($filter) { case 'blog/all': $title = pht('All Blogs'); $nodata = pht('No blogs have been created.'); break; case 'blog/user': $title = pht('Joinable Blogs'); $nodata = pht('There are no blogs you can contribute to.'); $query->requireCapabilities( array( PhabricatorPolicyCapability::CAN_JOIN, )); break; default: throw new Exception(pht("Unknown filter '%s'!", $filter)); } $pager = id(new PHUIPagerView()) ->setURI($request->getRequestURI(), 'offset') ->setOffset($request->getInt('offset')); $blogs = $query->executeWithOffsetPager($pager); $blog_list = $this->renderBlogList($blogs, $user, $nodata); $blog_list->setPager($pager); - $box = id (new PHUIObjectBoxView()) + $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setObjectList($blog_list); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, $this->getApplicationURI()); $nav->appendChild( array( $crumbs, $box, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function renderBlogList( array $blogs, - PhabricatorUser $user, + PhabricatorUser $viewer, $nodata) { $view = new PHUIObjectItemListView(); $view->setNoDataString($nodata); - $view->setUser($user); + $view->setUser($viewer); foreach ($blogs as $blog) { + $id = $blog->getID(); $item = id(new PHUIObjectItemView()) + ->setUser($viewer) + ->setObject($blog) ->setHeader($blog->getName()) - ->setHref($this->getApplicationURI('blog/view/'.$blog->getID().'/')) - ->setObject($blog); + ->setStatusIcon('fa-star') + ->setHref($this->getApplicationURI("/blog/view/{$id}/")) + ->addAttribute($blog->getSkin()) + ->addAttribute($blog->getDomain()); $view->addItem($item); } return $view; } } diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index 2196281c7d..c8eddfbad3 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -1,186 +1,179 @@ getUser(); $id = $request->getURIData('id'); $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($id)) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $posts = id(new PhamePostQuery()) ->setViewer($user) ->withBlogPHIDs(array($blog->getPHID())) ->executeWithCursorPager($pager); - $nav = $this->renderSideNavFilterView(null); - $header = id(new PHUIHeaderView()) ->setHeader($blog->getName()) ->setUser($user) ->setPolicyObject($blog); $actions = $this->renderActions($blog, $user); $properties = $this->renderProperties($blog, $user, $actions); $post_list = $this->renderPostList( $posts, $user, pht('This blog has no visible posts.')); - require_celerity_resource('phame-css'); - $post_list = id(new PHUIBoxView()) - ->addPadding(PHUI::PADDING_LARGE) - ->addClass('phame-post-list') + $post_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Latest Posts')) ->appendChild($post_list); - $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Blogs'), $this->getApplicationURI('blog/')); $crumbs->addTextCrumb($blog->getName(), $this->getApplicationURI()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); - $nav->appendChild( + return $this->buildApplicationPage( array( $crumbs, $object_box, $post_list, - )); - - return $this->buildApplicationPage( - $nav, + ), array( 'title' => $blog->getName(), )); } private function renderProperties( PhameBlog $blog, PhabricatorUser $user, PhabricatorActionListView $actions) { require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips'); $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setObject($blog) ->setActionList($actions); $properties->addProperty( pht('Skin'), $blog->getSkin()); $properties->addProperty( pht('Domain'), $blog->getDomain()); $feed_uri = PhabricatorEnv::getProductionURI( $this->getApplicationURI('blog/feed/'.$blog->getID().'/')); $properties->addProperty( pht('Atom URI'), javelin_tag('a', array( 'href' => $feed_uri, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Atom URI does not support custom domains.'), 'size' => 320, ), ), $feed_uri)); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $blog); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $properties->addProperty( pht('Joinable By'), $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user) ->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION) ->process(); $properties->invokeWillRenderEvent(); if (strlen($blog->getDescription())) { $description = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($blog->getDescription()), 'default', $user); $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent($description); } return $properties; } private function renderActions(PhameBlog $blog, PhabricatorUser $user) { $actions = id(new PhabricatorActionListView()) ->setObject($blog) ->setObjectURI($this->getRequest()->getRequestURI()) ->setUser($user); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $blog, PhabricatorPolicyCapability::CAN_EDIT); $can_join = PhabricatorPolicyFilter::hasCapability( $user, $blog, PhabricatorPolicyCapability::CAN_JOIN); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID())) ->setName(pht('Write Post')) ->setDisabled(!$can_join) ->setWorkflow(!$can_join)); $actions->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setIcon('fa-globe') ->setHref($blog->getLiveURI()) ->setName(pht('View Live'))); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/')) ->setName(pht('Edit Blog')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-times') ->setHref($this->getApplicationURI('blog/delete/'.$blog->getID().'/')) ->setName(pht('Delete Blog')) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $actions; } } diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php index 2b3324f805..e79d435deb 100644 --- a/src/applications/phame/controller/post/PhamePostEditController.php +++ b/src/applications/phame/controller/post/PhamePostEditController.php @@ -1,208 +1,203 @@ getUser(); $id = $request->getURIData('id'); if ($id) { $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI('/post/view/'.$id.'/'); $submit_button = pht('Save Changes'); $page_title = pht('Edit Post'); $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $post->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } else { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($request->getInt('blog'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_JOIN, )) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $v_projects = array(); $post = PhamePost::initializePost($user, $blog); $cancel_uri = $this->getApplicationURI('/blog/view/'.$blog->getID().'/'); $submit_button = pht('Save Draft'); $page_title = pht('Create Post'); } $title = $post->getTitle(); $phame_title = $post->getPhameTitle(); $body = $post->getBody(); $comments_widget = $post->getCommentsWidget(); $e_title = true; $e_phame_title = true; $validation_exception = null; if ($request->isFormPost()) { $title = $request->getStr('title'); $phame_title = $request->getStr('phame_title'); $phame_title = PhabricatorSlug::normalize($phame_title); $body = $request->getStr('body'); $comments_widget = $request->getStr('comments_widget'); $v_projects = $request->getArr('projects'); $xactions = array( id(new PhamePostTransaction()) ->setTransactionType(PhamePostTransaction::TYPE_TITLE) ->setNewValue($title), id(new PhamePostTransaction()) ->setTransactionType(PhamePostTransaction::TYPE_PHAME_TITLE) ->setNewValue($phame_title), id(new PhamePostTransaction()) ->setTransactionType(PhamePostTransaction::TYPE_BODY) ->setNewValue($body), id(new PhamePostTransaction()) ->setTransactionType(PhamePostTransaction::TYPE_COMMENTS_WIDGET) ->setNewValue($comments_widget), ); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhamePostTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PhamePostEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($post, $xactions); $uri = $this->getApplicationURI('/post/view/'.$post->getID().'/'); return id(new AphrontRedirectResponse())->setURI($uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_title = $validation_exception->getShortMessage( PhamePostTransaction::TYPE_TITLE); $e_phame_title = $validation_exception->getShortMessage( PhamePostTransaction::TYPE_PHAME_TITLE); } } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($post->getBlogPHID())) ->executeOne(); $form = id(new AphrontFormView()) ->setUser($user) ->addHiddenInput('blog', $request->getInt('blog')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Blog')) ->setValue($handle->renderLink())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setName('title') ->setValue($title) ->setID('post-title') ->setError($e_title)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phame Title')) ->setName('phame_title') ->setValue(rtrim($phame_title, '/')) ->setID('post-phame-title') ->setCaption(pht('Up to 64 alphanumeric characters '. 'with underscores for spaces. '. 'Formatting is enforced.')) ->setError($e_phame_title)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Body')) ->setName('body') ->setValue($body) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setID('post-body') ->setUser($user) ->setDisableMacros(true)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Comments Widget')) ->setName('comments_widget') ->setvalue($comments_widget) ->setOptions($post->getCommentsWidgetOptionsForSelect())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $loading = phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')); $preview_panel = phutil_tag_div('aphront-panel-preview', array( phutil_tag_div('phame-post-preview-header', pht('Post Preview')), phutil_tag('div', array('id' => 'post-preview'), $loading), )); - require_celerity_resource('phame-css'); Javelin::initBehavior( 'phame-post-preview', array( 'preview' => 'post-preview', 'body' => 'post-body', 'title' => 'post-title', 'phame_title' => 'post-phame-title', 'uri' => '/phame/post/preview/', )); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $page_title, $this->getApplicationURI('/post/view/'.$id.'/')); - $nav = $this->renderSideNavFilterView(null); - $nav->appendChild( + return $this->buildApplicationPage( array( $crumbs, $form_box, $preview_panel, - )); - - return $this->buildApplicationPage( - $nav, + ), array( 'title' => $page_title, )); } } diff --git a/src/applications/phame/controller/post/PhamePostListController.php b/src/applications/phame/controller/post/PhamePostListController.php index 6a02129bd8..b2b28bd4eb 100644 --- a/src/applications/phame/controller/post/PhamePostListController.php +++ b/src/applications/phame/controller/post/PhamePostListController.php @@ -1,80 +1,79 @@ getViewer(); $filter = $request->getURIData('filter'); $bloggername = $request->getURIData('bloggername'); $query = id(new PhamePostQuery()) ->setViewer($viewer); $nav = $this->renderSideNavFilterView(); $nodata = null; switch ($filter) { case 'draft': $query->withBloggerPHIDs(array($viewer->getPHID())); $query->withVisibility(PhamePost::VISIBILITY_DRAFT); $nodata = pht('You have no unpublished drafts.'); $title = pht('Unpublished Drafts'); $nav->selectFilter('post/draft'); break; - case 'all': - $nodata = pht('There are no visible posts.'); - $title = pht('Posts'); - $nav->selectFilter('post/all'); - break; - default: case 'blogger': if ($bloggername) { $blogger = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $bloggername); if (!$blogger) { return new Aphront404Response(); } } else { $blogger = $viewer; } $query->withBloggerPHIDs(array($blogger->getPHID())); if ($blogger->getPHID() == $viewer->getPHID()) { $nav->selectFilter('post'); $nodata = pht('You have not written any posts.'); } else { $nodata = pht('%s has not written any posts.', $blogger); } $title = pht('Posts by %s', $blogger); break; + default: + case 'all': + $nodata = pht('There are no visible posts.'); + $title = pht('Posts'); + $nav->selectFilter('post/all'); + break; } $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $posts = $query->executeWithCursorPager($pager); - require_celerity_resource('phame-css'); $post_list = $this->renderPostList($posts, $viewer, $nodata); $post_list = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->appendChild($post_list); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, $this->getApplicationURI()); $nav->appendChild( array( $crumbs, $post_list, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } } diff --git a/src/applications/phame/controller/post/PhamePostNewController.php b/src/applications/phame/controller/post/PhamePostNewController.php index 94d8651f27..33330ef271 100644 --- a/src/applications/phame/controller/post/PhamePostNewController.php +++ b/src/applications/phame/controller/post/PhamePostNewController.php @@ -1,122 +1,120 @@ getUser(); $id = $request->getURIData('id'); $post = null; $view_uri = null; if ($id) { $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $view_uri = '/post/view/'.$post->getID().'/'; $view_uri = $this->getApplicationURI($view_uri); if ($request->isFormPost()) { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($request->getInt('blog'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_JOIN, )) ->executeOne(); if ($blog) { $post->setBlogPHID($blog->getPHID()); $post->save(); return id(new AphrontRedirectResponse())->setURI($view_uri); } } $title = pht('Move Post'); } else { $title = pht('Create Post'); $view_uri = $this->getApplicationURI('/post/new'); } $blogs = id(new PhameBlogQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_JOIN, )) ->execute(); - $nav = $this->renderSideNavFilterView(); - $nav->selectFilter('post/new'); - $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, $view_uri); - $nav->appendChild($crumbs); + $display = array(); + $display[] = $crumbs; if (!$blogs) { $notification = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NODATA) ->appendChild( pht('You do not have permission to join any blogs. Create a blog '. 'first, then you can post to it.')); - $nav->appendChild($notification); + $display[] = $notification; } else { $options = mpull($blogs, 'getName', 'getID'); asort($options); $selected_value = null; if ($post && $post->getBlog()) { $selected_value = $post->getBlog()->getID(); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Blog')) ->setName('blog') ->setOptions($options) ->setValue($selected_value)); if ($post) { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Move Post')) ->addCancelButton($view_uri)); } else { $form ->setAction($this->getApplicationURI('post/edit/')) ->setMethod('GET') ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue'))); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); - $nav->appendChild($form_box); + $display[] = $form_box; } return $this->buildApplicationPage( - $nav, + $display, array( 'title' => $title, )); } } diff --git a/src/applications/phame/query/PhamePostSearchEngine.php b/src/applications/phame/query/PhamePostSearchEngine.php index 600d633627..a5feb3d634 100644 --- a/src/applications/phame/query/PhamePostSearchEngine.php +++ b/src/applications/phame/query/PhamePostSearchEngine.php @@ -1,110 +1,110 @@ newQuery(); if (strlen($map['visibility'])) { $query->withVisibility($map['visibility']); } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorSearchSelectField()) ->setKey('visibility') ->setOptions(array( '' => pht('All'), PhamePost::VISIBILITY_PUBLISHED => pht('Live'), PhamePost::VISIBILITY_DRAFT => pht('Draft'), )), ); } protected function getURI($path) { return '/phame/post/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All'), 'live' => pht('Live'), 'draft' => pht('Draft'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'live': return $query->setParameter( 'visibility', PhamePost::VISIBILITY_PUBLISHED); case 'draft': return $query->setParameter( 'visibility', PhamePost::VISIBILITY_DRAFT); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $posts, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($posts, 'PhamePost'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($posts as $post) { $id = $post->getID(); $blog = $viewer->renderHandle($post->getBlogPHID())->render(); $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setObject($post) ->setHeader($post->getTitle()) ->setStatusIcon('fa-star') ->setHref($this->getApplicationURI("/post/view/{$id}/")) ->addAttribute( pht('Blog: %s', $blog)); if ($post->isDraft()) { $item->setStatusIcon('fa-star-o grey'); $item->setDisabled(true); $item->addIcon('none', pht('Draft Post')); } else { $date = $post->getDatePublished(); $item->setEpoch($date); } $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); - $result->setNoDataString(pht('No blogs found.')); + $result->setNoDataString(pht('No blogs posts found.')); return $result; } } diff --git a/src/applications/phame/view/PhamePostView.php b/src/applications/phame/view/PhamePostView.php index 236ecc2b39..f35125f255 100644 --- a/src/applications/phame/view/PhamePostView.php +++ b/src/applications/phame/view/PhamePostView.php @@ -1,240 +1,241 @@ skin = $skin; return $this; } public function getSkin() { return $this->skin; } public function setAuthor(PhabricatorObjectHandle $author) { $this->author = $author; return $this; } public function getAuthor() { return $this->author; } public function setPost(PhamePost $post) { $this->post = $post; return $this; } public function getPost() { return $this->post; } public function setBody($body) { $this->body = $body; return $this; } public function getBody() { return $this->body; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { return $this->summary; } public function renderTitle() { $href = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); return phutil_tag( 'h2', array( 'class' => 'phame-post-title', ), phutil_tag( 'a', array( 'href' => $href, ), $this->getPost()->getTitle())); } public function renderDatePublished() { return phutil_tag( 'div', array( 'class' => 'phame-post-date', ), pht( 'Published on %s by %s', phabricator_datetime( $this->getPost()->getDatePublished(), $this->getUser()), $this->getAuthor()->getName())); } public function renderBody() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getBody()); } public function renderSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getSummary()); } public function renderComments() { $post = $this->getPost(); switch ($post->getCommentsWidget()) { case 'facebook': $comments = $this->renderFacebookComments(); break; case 'disqus': $comments = $this->renderDisqusComments(); break; case 'none': default: $comments = null; break; } return $comments; } public function render() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderBody(), $this->renderComments(), )); } public function renderWithSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderSummary(), )); } private function renderFacebookComments() { $fb_id = PhabricatorFacebookAuthProvider::getFacebookApplicationID(); if (!$fb_id) { return null; } $fb_root = phutil_tag('div', array( 'id' => 'fb-root', ), ''); $c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id; $fb_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( '(function(d, s, id) {'. ' var js, fjs = d.getElementsByTagName(s)[0];'. ' if (d.getElementById(id)) return;'. ' js = d.createElement(s); js.id = id;'. ' js.src = %s;'. ' fjs.parentNode.insertBefore(js, fjs);'. '}(document, \'script\', \'facebook-jssdk\'));', $c_uri)); $uri = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); + require_celerity_resource('phame-css'); $fb_comments = phutil_tag('div', array( 'class' => 'fb-comments', 'data-href' => $uri, 'data-num-posts' => 5, ), ''); return phutil_tag( 'div', array( 'class' => 'phame-comments-facebook', ), array( $fb_root, $fb_js, $fb_comments, )); } private function renderDisqusComments() { $disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname'); if (!$disqus_shortname) { return null; } $post = $this->getPost(); $disqus_thread = phutil_tag('div', array( 'id' => 'disqus_thread', )); // protip - try some var disqus_developer = 1; action to test locally $disqus_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( ' var disqus_shortname = %s;'. ' var disqus_identifier = %s;'. ' var disqus_url = %s;'. ' var disqus_title = %s;'. '(function() {'. ' var dsq = document.createElement("script");'. ' dsq.type = "text/javascript";'. ' dsq.async = true;'. ' dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";'. '(document.getElementsByTagName("head")[0] ||'. ' document.getElementsByTagName("body")[0]).appendChild(dsq);'. '})();', $disqus_shortname, $post->getPHID(), $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()), $post->getTitle())); return phutil_tag( 'div', array( 'class' => 'phame-comments-disqus', ), array( $disqus_thread, $disqus_js, )); } } diff --git a/src/applications/pholio/controller/PholioImageUploadController.php b/src/applications/pholio/controller/PholioImageUploadController.php index 530563dc8f..0329d3eb1d 100644 --- a/src/applications/pholio/controller/PholioImageUploadController.php +++ b/src/applications/pholio/controller/PholioImageUploadController.php @@ -1,44 +1,43 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $phid = $request->getStr('filePHID'); $replaces_phid = $request->getStr('replacesPHID'); $title = $request->getStr('title'); $description = $request->getStr('description'); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } if (!strlen($title)) { $title = $file->getName(); } $image = id(new PholioImage()) ->attachFile($file) ->setName($title) ->setDescription($description) ->makeEphemeral(); $view = id(new PholioUploadedImageView()) ->setUser($viewer) ->setImage($image) ->setReplacesPHID($replaces_phid); $content = array( 'markup' => $view, ); return id(new AphrontAjaxResponse())->setContent($content); } } diff --git a/src/applications/pholio/controller/PholioInlineController.php b/src/applications/pholio/controller/PholioInlineController.php index 4cc5510b99..101ef9e758 100644 --- a/src/applications/pholio/controller/PholioInlineController.php +++ b/src/applications/pholio/controller/PholioInlineController.php @@ -1,173 +1,167 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { - $inline = id(new PholioTransactionComment())->load($this->id); + if ($id) { + $inline = id(new PholioTransactionComment())->load($id); if (!$inline) { return new Aphront404Response(); } if ($inline->getTransactionPHID()) { $mode = 'view'; } else { if ($inline->getAuthorPHID() == $viewer->getPHID()) { $mode = 'edit'; } else { return new Aphront404Response(); } } } else { $mock = id(new PholioMockQuery()) ->setViewer($viewer) ->withIDs(array($request->getInt('mockID'))) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $inline = id(new PholioTransactionComment()) ->setImageID($request->getInt('imageID')) ->setX($request->getInt('startX')) ->setY($request->getInt('startY')) ->setCommentVersion(1) ->setAuthorPHID($viewer->getPHID()) ->setEditPolicy($viewer->getPHID()) ->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC) ->setContentSourceFromRequest($request) ->setWidth($request->getInt('endX') - $request->getInt('startX')) ->setHeight($request->getInt('endY') - $request->getInt('startY')); $mode = 'new'; } $v_content = $inline->getContent(); // TODO: Not correct, but we don't always have a mock right now. $mock_uri = '/'; if ($mode == 'view') { require_celerity_resource('pholio-inline-comments-css'); $image = id(new PholioImageQuery()) ->setViewer($viewer) ->withIDs(array($inline->getImageID())) ->executeOne(); $handles = $this->loadViewerHandles(array($inline->getAuthorPHID())); $author_handle = $handles[$inline->getAuthorPHID()]; $file = $image->getFile(); if (!$file->isViewableImage()) { throw new Exception(pht('File is not viewable.')); } $image_uri = $file->getBestURI(); $thumb = id(new PHUIImageMaskView()) ->addClass('mrl') ->setImage($image_uri) ->setDisplayHeight(200) ->setDisplayWidth(498) ->withMask(true) ->centerViewOnPoint( $inline->getX(), $inline->getY(), $inline->getHeight(), $inline->getWidth()); $comment_head = phutil_tag( 'div', array( 'class' => 'pholio-inline-comment-head', ), $author_handle->renderLink()); $comment_body = phutil_tag( 'div', array( 'class' => 'pholio-inline-comment-body', ), PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent($inline->getContent()), 'default', $viewer)); return $this->newDialog() ->setTitle(pht('Inline Comment')) ->appendChild($thumb) ->appendChild($comment_head) ->appendChild($comment_body) ->addCancelButton($mock_uri, pht('Close')); } if ($request->isFormPost()) { $v_content = $request->getStr('content'); if (strlen($v_content)) { $inline->setContent($v_content); $inline->save(); $dictionary = $inline->toDictionary(); } else if ($inline->getID()) { $inline->delete(); $dictionary = array(); } return id(new AphrontAjaxResponse())->setContent($dictionary); } switch ($mode) { case 'edit': $title = pht('Edit Inline Comment'); $submit_text = pht('Save Draft'); break; case 'new': $title = pht('New Inline Comment'); $submit_text = pht('Save Draft'); break; } $form = id(new AphrontFormView()) ->setUser($viewer); if ($mode == 'new') { $params = array( 'mockID' => $request->getInt('mockID'), 'imageID' => $request->getInt('imageID'), 'startX' => $request->getInt('startX'), 'startY' => $request->getInt('startY'), 'endX' => $request->getInt('endX'), 'endY' => $request->getInt('endY'), ); foreach ($params as $key => $value) { $form->addHiddenInput($key, $value); } } $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('content') ->setLabel(pht('Comment')) ->setValue($v_content)); return $this->newDialog() ->setTitle($title) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($form->buildLayoutView()) ->addCancelButton($mock_uri) ->addSubmitButton($submit_text); } } diff --git a/src/applications/pholio/controller/PholioInlineListController.php b/src/applications/pholio/controller/PholioInlineListController.php index 0befd5dbd2..673a55bbaa 100644 --- a/src/applications/pholio/controller/PholioInlineListController.php +++ b/src/applications/pholio/controller/PholioInlineListController.php @@ -1,40 +1,34 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $image = id(new PholioImageQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$image) { return new Aphront404Response(); } $inline_comments = id(new PholioTransactionComment())->loadAllWhere( 'imageid = %d AND (transactionphid IS NOT NULL OR (authorphid = %s AND transactionphid IS NULL))', - $this->id, - $user->getPHID()); + $id, + $viewer->getPHID()); $author_phids = mpull($inline_comments, 'getAuthorPHID'); $authors = $this->loadViewerHandles($author_phids); $inlines = array(); foreach ($inline_comments as $inline_comment) { $inlines[] = $inline_comment->toDictionary(); } return id(new AphrontAjaxResponse())->setContent($inlines); } } diff --git a/src/applications/pholio/controller/PholioMockCommentController.php b/src/applications/pholio/controller/PholioMockCommentController.php index 719d2db6ef..b127d0b1da 100644 --- a/src/applications/pholio/controller/PholioMockCommentController.php +++ b/src/applications/pholio/controller/PholioMockCommentController.php @@ -1,90 +1,84 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $mock = id(new PholioMockQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needImages(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $mock_uri = '/M'.$mock->getID(); $comment = $request->getStr('comment'); $xactions = array(); $inline_comments = id(new PholioTransactionComment())->loadAllWhere( 'authorphid = %s AND transactionphid IS NULL AND imageid IN (%Ld)', - $user->getPHID(), + $viewer->getPHID(), mpull($mock->getImages(), 'getID')); if (!$inline_comments || strlen($comment)) { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PholioTransactionComment()) ->setContent($comment)); } foreach ($inline_comments as $inline_comment) { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_INLINE) ->attachComment($inline_comment); } $editor = id(new PholioMockEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect($request->isContinueRequest()) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($mock, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($mock_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { $xaction_view = id(new PholioTransactionView()) ->setMock($mock); return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setTransactionView($xaction_view) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse())->setURI($mock_uri); } } } diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index 728c7b29d6..18423e0050 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -1,400 +1,394 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->id) { + if ($id) { $mock = id(new PholioMockQuery()) - ->setViewer($user) + ->setViewer($viewer) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $title = pht('Edit Mock'); $is_new = false; $mock_images = $mock->getImages(); $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { - $mock = PholioMock::initializeNewMock($user); + $mock = PholioMock::initializeNewMock($viewer); $title = pht('Create Mock'); $is_new = true; $files = array(); $mock_images = array(); } if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $e_name = true; $e_images = count($mock_images) ? null : true; $errors = array(); $posted_mock_images = array(); $v_name = $mock->getName(); $v_desc = $mock->getDescription(); $v_status = $mock->getStatus(); $v_view = $mock->getViewPolicy(); $v_edit = $mock->getEditPolicy(); $v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID( $mock->getPHID()); $v_space = $mock->getSpacePHID(); if ($request->isFormPost()) { $xactions = array(); $type_name = PholioTransaction::TYPE_NAME; $type_desc = PholioTransaction::TYPE_DESCRIPTION; $type_status = PholioTransaction::TYPE_STATUS; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_cc = PhabricatorTransactions::TYPE_SUBSCRIBERS; $type_space = PhabricatorTransactions::TYPE_SPACE; $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_status = $request->getStr('status'); $v_view = $request->getStr('can_view'); $v_edit = $request->getStr('can_edit'); $v_cc = $request->getArr('cc'); $v_projects = $request->getArr('projects'); $v_space = $request->getStr('spacePHID'); $mock_xactions = array(); $mock_xactions[$type_name] = $v_name; $mock_xactions[$type_desc] = $v_desc; $mock_xactions[$type_status] = $v_status; $mock_xactions[$type_view] = $v_view; $mock_xactions[$type_edit] = $v_edit; $mock_xactions[$type_cc] = array('=' => $v_cc); $mock_xactions[$type_space] = $v_space; if (!strlen($request->getStr('name'))) { $e_name = pht('Required'); $errors[] = pht('You must give the mock a name.'); } $file_phids = $request->getArr('file_phids'); if ($file_phids) { $files = id(new PhabricatorFileQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $files = array_select_keys($files, $file_phids); } else { $files = array(); } if (!$files) { $e_images = pht('Required'); $errors[] = pht('You must add at least one image to the mock.'); } else { $mock->setCoverPHID(head($files)->getPHID()); } foreach ($mock_xactions as $type => $value) { $xactions[$type] = id(new PholioTransaction()) ->setTransactionType($type) ->setNewValue($value); } $order = $request->getStrList('imageOrder'); $sequence_map = array_flip($order); $replaces = $request->getArr('replaces'); $replaces_map = array_flip($replaces); /** * Foreach file posted, check to see whether we are replacing an image, * adding an image, or simply updating image metadata. Create * transactions for these cases as appropos. */ foreach ($files as $file_phid => $file) { $replaces_image_phid = null; if (isset($replaces_map[$file_phid])) { $old_file_phid = $replaces_map[$file_phid]; if ($old_file_phid != $file_phid) { $old_image = idx($mock_images, $old_file_phid); if ($old_image) { $replaces_image_phid = $old_image->getPHID(); } } } $existing_image = idx($mock_images, $file_phid); $title = (string)$request->getStr('title_'.$file_phid); $description = (string)$request->getStr('description_'.$file_phid); $sequence = $sequence_map[$file_phid]; if ($replaces_image_phid) { $replace_image = id(new PholioImage()) ->setReplacesImagePHID($replaces_image_phid) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransaction::TYPE_IMAGE_REPLACE) ->setNewValue($replace_image); $posted_mock_images[] = $replace_image; } else if (!$existing_image) { // this is an add $add_image = id(new PholioImage()) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_IMAGE_FILE) ->setNewValue( array('+' => array($add_image))); $posted_mock_images[] = $add_image; } else { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_IMAGE_NAME) ->setNewValue( array($existing_image->getPHID() => $title)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransaction::TYPE_IMAGE_DESCRIPTION) ->setNewValue( array($existing_image->getPHID() => $description)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransaction::TYPE_IMAGE_SEQUENCE) ->setNewValue( array($existing_image->getPHID() => $sequence)); $posted_mock_images[] = $existing_image; } } foreach ($mock_images as $file_phid => $mock_image) { if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) { // this is an outright delete $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_IMAGE_FILE) ->setNewValue( array('-' => array($mock_image))); } } if (!$errors) { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PholioTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $mock->openTransaction(); $editor = id(new PholioMockEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) - ->setActor($user); + ->setActor($viewer); $xactions = $editor->applyTransactions($mock, $xactions); $mock->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI('/M'.$mock->getID()); } } - if ($this->id) { + if ($id) { $submit = id(new AphrontFormSubmitControl()) - ->addCancelButton('/M'.$this->id) + ->addCancelButton('/M'.$id) ->setValue(pht('Save')); } else { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Create')); } $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($mock) ->execute(); // NOTE: Make this show up correctly on the rendered form. $mock->setViewPolicy($v_view); $mock->setEditPolicy($v_edit); $image_elements = array(); if ($posted_mock_images) { $display_mock_images = $posted_mock_images; } else { $display_mock_images = $mock_images; } foreach ($display_mock_images as $mock_image) { $image_elements[] = id(new PholioUploadedImageView()) - ->setUser($user) + ->setUser($viewer) ->setImage($mock_image) ->setReplacesPHID($mock_image->getFilePHID()); } $list_id = celerity_generate_unique_node_id(); $drop_id = celerity_generate_unique_node_id(); $order_id = celerity_generate_unique_node_id(); $list_control = phutil_tag( 'div', array( 'id' => $list_id, 'class' => 'pholio-edit-list', ), $image_elements); $drop_control = phutil_tag( 'div', array( 'id' => $drop_id, 'class' => 'pholio-edit-drop', ), pht('Drag and drop images here to add them to the mock.')); $order_control = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'imageOrder', 'id' => $order_id, )); Javelin::initBehavior( 'pholio-mock-edit', array( 'listID' => $list_id, 'dropID' => $drop_id, 'orderID' => $order_id, 'uploadURI' => '/file/dropupload/', 'renderURI' => $this->getApplicationURI('image/upload/'), 'pht' => array( 'uploading' => pht('Uploading Image...'), 'uploaded' => pht('Upload Complete...'), 'undo' => pht('Undo'), 'removed' => pht('This image will be removed from the mock.'), ), )); require_celerity_resource('pholio-edit-css'); $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendChild($order_control) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setValue($v_name) ->setLabel(pht('Name')) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setValue($v_desc) ->setLabel(pht('Description')) - ->setUser($user)); + ->setUser($viewer)); - if ($this->id) { + if ($id) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setValue($mock->getStatus()) ->setOptions($mock->getStatuses())); } else { $form->addHiddenInput('status', 'open'); } $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('cc') ->setValue($v_cc) - ->setUser($user) + ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource())) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($mock) ->setPolicies($policies) ->setSpacePHID($v_space) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($mock) ->setPolicies($policies) ->setName('can_edit')) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($list_control)) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($drop_control) ->setError($e_images)) ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$is_new) { $crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram()); } $crumbs->addTextCrumb($title); $content = array( $crumbs, $form_box, ); $this->addExtraQuicksandConfig( array('mockEditConfig' => true)); return $this->buildApplicationPage( $content, array( 'title' => $title, )); } } diff --git a/src/applications/pholio/controller/PholioMockListController.php b/src/applications/pholio/controller/PholioMockListController.php index c4d7fb4117..b4fa17fc8a 100644 --- a/src/applications/pholio/controller/PholioMockListController.php +++ b/src/applications/pholio/controller/PholioMockListController.php @@ -1,24 +1,20 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PholioMockSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index b9084474d2..c5c6fed16c 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -1,217 +1,211 @@ maniphestTaskPHIDs = $maniphest_task_phids; return $this; } private function getManiphestTaskPHIDs() { return $this->maniphestTaskPHIDs; } public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->id = $data['id']; - $this->imageID = idx($data, 'imageID'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $image_id = $request->getURIData('imageID'); $mock = id(new PholioMockQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needImages(true) ->needInlineComments(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PholioMockHasTaskEdgeType::EDGECONST); $this->setManiphestTaskPHIDs($phids); $engine = id(new PhabricatorMarkupEngine()) - ->setViewer($user); + ->setViewer($viewer); $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); $title = $mock->getName(); if ($mock->isClosed()) { $header_icon = 'fa-ban'; $header_name = pht('Closed'); $header_color = 'dark'; } else { $header_icon = 'fa-square-o'; $header_name = pht('Open'); $header_color = 'bluegrey'; } $header = id(new PHUIHeaderView()) ->setHeader($title) - ->setUser($user) + ->setUser($viewer) ->setStatus($header_icon, $header_color, $header_name) ->setPolicyObject($mock); $timeline = $this->buildTransactionTimeline( $mock, new PholioTransactionQuery(), $engine); $timeline->setMock($mock); $actions = $this->buildActionView($mock); $properties = $this->buildPropertyView($mock, $engine, $actions); require_celerity_resource('pholio-css'); require_celerity_resource('pholio-inline-comments-css'); $comment_form_id = celerity_generate_unique_node_id(); $mock_view = id(new PholioMockImagesView()) ->setRequestURI($request->getRequestURI()) ->setCommentFormID($comment_form_id) - ->setUser($user) + ->setUser($viewer) ->setMock($mock) - ->setImageID($this->imageID); + ->setImageID($image_id); $this->addExtraQuicksandConfig( array('mockViewConfig' => $mock_view->getBehaviorConfig())); $output = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Image')) ->appendChild($mock_view); $add_comment = $this->buildAddCommentView($mock, $comment_form_id); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $thumb_grid = id(new PholioMockThumbGridView()) - ->setUser($user) + ->setUser($viewer) ->setMock($mock); $content = array( $crumbs, $object_box, $output, $thumb_grid, $timeline, $add_comment, ); return $this->buildApplicationPage( $content, array( 'title' => 'M'.$mock->getID().' '.$title, 'pageObjects' => array($mock->getPHID()), )); } private function buildActionView(PholioMock $mock) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $actions = id(new PhabricatorActionListView()) - ->setUser($user) + ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($mock); $can_edit = PhabricatorPolicyFilter::hasCapability( - $user, + $viewer, $mock, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Mock')) ->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-anchor') ->setName(pht('Edit Maniphest Tasks')) ->setHref("/search/attach/{$mock->getPHID()}/TASK/edge/") - ->setDisabled(!$user->isLoggedIn()) + ->setDisabled(!$viewer->isLoggedIn()) ->setWorkflow(true)); return $actions; } private function buildPropertyView( PholioMock $mock, PhabricatorMarkupEngine $engine, PhabricatorActionListView $actions) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($user) + ->setUser($viewer) ->setObject($mock) ->setActionList($actions); $properties->addProperty( pht('Author'), - $user->renderHandle($mock->getAuthorPHID())); + $viewer->renderHandle($mock->getAuthorPHID())); $properties->addProperty( pht('Created'), - phabricator_datetime($mock->getDateCreated(), $user)); + phabricator_datetime($mock->getDateCreated(), $viewer)); if ($this->getManiphestTaskPHIDs()) { $properties->addProperty( pht('Maniphest Tasks'), - $user->renderHandleList($this->getManiphestTaskPHIDs())); + $viewer->renderHandleList($this->getManiphestTaskPHIDs())); } $properties->invokeWillRenderEvent(); $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addImageContent( $engine->getOutput($mock, PholioMock::MARKUP_FIELD_DESCRIPTION)); return $properties; } private function buildAddCommentView(PholioMock $mock, $comment_form_id) { - $user = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); - $draft = PhabricatorDraft::newFromUserAndKey($user, $mock->getPHID()); + $draft = PhabricatorDraft::newFromUserAndKey($viewer, $mock->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('History Beckons'); $form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setObjectPHID($mock->getPHID()) ->setFormID($comment_form_id) ->setDraft($draft) ->setHeaderText($title) ->setSubmitButtonName(pht('Add Comment')) ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/pholio/herald/HeraldPholioMockAdapter.php b/src/applications/pholio/herald/HeraldPholioMockAdapter.php index 2c87c394e9..57fa74c3a1 100644 --- a/src/applications/pholio/herald/HeraldPholioMockAdapter.php +++ b/src/applications/pholio/herald/HeraldPholioMockAdapter.php @@ -1,76 +1,54 @@ mock = $this->newObject(); } protected function newObject() { return new PholioMock(); } public function getObject() { return $this->mock; } public function setMock(PholioMock $mock) { $this->mock = $mock; return $this; } public function getMock() { return $this->mock; } public function getAdapterContentName() { return pht('Pholio Mocks'); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_FLAG, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } - } - public function getHeraldName() { return 'M'.$this->getMock()->getID(); } } diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php index 8e9d3007c5..d7d174d928 100644 --- a/src/applications/pholio/view/PholioMockThumbGridView.php +++ b/src/applications/pholio/view/PholioMockThumbGridView.php @@ -1,177 +1,180 @@ mock = $mock; return $this; } public function render() { $mock = $this->mock; $all_images = $mock->getAllImages(); $all_images = mpull($all_images, null, 'getPHID'); $history = mpull($all_images, 'getReplacesImagePHID', 'getPHID'); $replaced = array(); foreach ($history as $phid => $replaces_phid) { if ($replaces_phid) { $replaced[$replaces_phid] = true; } } // Figure out the columns. Start with all the active images. $images = mpull($mock->getImages(), null, 'getPHID'); // Now, find deleted images: obsolete images which were not replaced. foreach ($mock->getAllImages() as $image) { if (!$image->getIsObsolete()) { // Image is current. continue; } if (isset($replaced[$image->getPHID()])) { // Image was replaced. continue; } // This is an obsolete image which was not replaced, so it must be // a deleted image. $images[$image->getPHID()] = $image; } $cols = array(); $depth = 0; foreach ($images as $image) { $phid = $image->getPHID(); $col = array(); // If this is a deleted image, null out the final column. if ($image->getIsObsolete()) { $col[] = null; } $col[] = $phid; while ($phid && isset($history[$phid])) { $col[] = $history[$phid]; $phid = $history[$phid]; } $cols[] = $col; $depth = max($depth, count($col)); } $grid = array(); $jj = $depth; for ($ii = 0; $ii < $depth; $ii++) { $row = array(); if ($depth == $jj) { $row[] = phutil_tag( 'th', array( 'valign' => 'middle', 'class' => 'pholio-history-header', ), pht('Current Revision')); } else { $row[] = phutil_tag('th', array(), null); } foreach ($cols as $col) { if (empty($col[$ii])) { $row[] = phutil_tag('td', array(), null); } else { $thumb = $this->renderThumbnail($all_images[$col[$ii]]); $row[] = phutil_tag('td', array(), $thumb); } } $grid[] = phutil_tag('tr', array(), $row); $jj--; } $grid = phutil_tag( 'table', array( 'id' => 'pholio-mock-thumb-grid', 'class' => 'pholio-mock-thumb-grid', ), $grid); $grid = id(new PHUIBoxView()) ->addClass('pholio-mock-thumb-grid-container') ->appendChild($grid); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Mock History')) ->appendChild($grid); } private function renderThumbnail(PholioImage $image) { $thumbfile = $image->getFile(); $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_THUMBGRID; $xform = PhabricatorFileTransform::getTransformByKey($preview_key); + Javelin::initBehavior('phabricator-tooltips'); $attributes = array( 'class' => 'pholio-mock-thumb-grid-image', 'src' => $thumbfile->getURIForTransform($xform), ); if ($image->getFile()->isViewableImage()) { $dimensions = $xform->getTransformedDimensions($thumbfile); if ($dimensions) { list($x, $y) = $dimensions; $attributes += array( 'width' => $x, 'height' => $y, 'style' => 'top: '.floor((100 - $y) / 2).'px', ); } } else { // If this is a PDF or a text file or something, we'll end up using a // generic thumbnail which is always sized correctly. $attributes += array( 'width' => 100, 'height' => 100, ); } $tag = phutil_tag('img', $attributes); $classes = array('pholio-mock-thumb-grid-item'); if ($image->getIsObsolete()) { $classes[] = 'pholio-mock-thumb-grid-item-obsolete'; } $inline_count = null; if ($image->getInlineComments()) { $inline_count[] = phutil_tag( 'span', array( 'class' => 'pholio-mock-thumb-grid-comment-count', ), pht('%s', new PhutilNumber(count($image->getInlineComments())))); } return javelin_tag( 'a', array( - 'sigil' => 'mock-thumbnail', + 'sigil' => 'mock-thumbnail has-tooltip', 'class' => implode(' ', $classes), 'href' => '#', 'meta' => array( 'imageID' => $image->getID(), + 'tip' => $image->getName(), + 'align' => 'N', ), ), array( $tag, $inline_count, )); } } diff --git a/src/applications/phortune/controller/PhortuneAccountEditController.php b/src/applications/phortune/controller/PhortuneAccountEditController.php index 7847420549..f51f1293d9 100644 --- a/src/applications/phortune/controller/PhortuneAccountEditController.php +++ b/src/applications/phortune/controller/PhortuneAccountEditController.php @@ -1,135 +1,129 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { + if ($id) { $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return new Aphront404Response(); } $is_new = false; } else { $account = PhortuneAccount::initializeNewAccount($viewer); $account->attachMemberPHIDs(array($viewer->getPHID())); $is_new = true; } $v_name = $account->getName(); $e_name = true; $v_members = $account->getMemberPHIDs(); $e_members = null; $validation_exception = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_members = $request->getArr('memberPHIDs'); $type_name = PhortuneAccountTransaction::TYPE_NAME; $type_edge = PhabricatorTransactions::TYPE_EDGE; $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType($type_edge) ->setMetadataValue( 'edge:type', PhortuneAccountHasMemberEdgeType::EDGECONST) ->setNewValue( array( '=' => array_fuse($v_members), )); $editor = id(new PhortuneAccountEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($account, $xactions); $account_uri = $this->getApplicationURI($account->getID().'/'); return id(new AphrontRedirectResponse())->setURI($account_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); $e_members = $ex->getShortMessage($type_edge); } } $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $cancel_uri = $this->getApplicationURI('account/'); $crumbs->addTextCrumb(pht('Accounts'), $cancel_uri); $crumbs->addTextCrumb(pht('Create Account')); $title = pht('Create Payment Account'); $submit_button = pht('Create Account'); } else { $cancel_uri = $this->getApplicationURI($account->getID().'/'); $crumbs->addTextCrumb($account->getName(), $cancel_uri); $crumbs->addTextCrumb(pht('Edit')); $title = pht('Edit %s', $account->getName()); $submit_button = pht('Save Changes'); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setLabel(pht('Members')) ->setName('memberPHIDs') ->setValue($v_members) ->setError($e_members)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($submit_button) ->addCancelButton($cancel_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneAccountListController.php b/src/applications/phortune/controller/PhortuneAccountListController.php index 8613cbda4d..0de082fa01 100644 --- a/src/applications/phortune/controller/PhortuneAccountListController.php +++ b/src/applications/phortune/controller/PhortuneAccountListController.php @@ -1,104 +1,103 @@ getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $accounts = id(new PhortuneAccountQuery()) ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $merchants = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->execute(); $title = pht('Accounts'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Accounts')); $payment_list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht( 'You are not a member of any payment accounts. Payment '. 'accounts are used to make purchases.')); foreach ($accounts as $account) { $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Account %d', $account->getID())) ->setHeader($account->getName()) ->setHref($this->getApplicationURI($account->getID().'/')) ->setObject($account); $payment_list->addItem($item); } $payment_header = id(new PHUIHeaderView()) ->setHeader(pht('Payment Accounts')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($this->getApplicationURI('account/edit/')) ->setIcon( id(new PHUIIconView()) ->setIconFont('fa-plus')) ->setText(pht('Create Account'))); $payment_box = id(new PHUIObjectBoxView()) ->setHeader($payment_header) ->setObjectList($payment_list); $merchant_list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht( 'You do not control any merchant accounts. Merchant accounts are '. 'used to receive payments.')); foreach ($merchants as $merchant) { $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Merchant %d', $merchant->getID())) ->setHeader($merchant->getName()) ->setHref($this->getApplicationURI('/merchant/'.$merchant->getID().'/')) ->setObject($merchant); $merchant_list->addItem($item); } $merchant_header = id(new PHUIHeaderView()) ->setHeader(pht('Merchant Accounts')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($this->getApplicationURI('merchant/')) ->setIcon( id(new PHUIIconView()) ->setIconFont('fa-list')) ->setText(pht('View All Merchants'))); $merchant_box = id(new PHUIObjectBoxView()) ->setHeader($merchant_header) ->setObjectList($merchant_list); return $this->buildApplicationPage( array( $crumbs, $payment_box, $merchant_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneCartAcceptController.php b/src/applications/phortune/controller/PhortuneCartAcceptController.php index 3db7467290..cb53e66f50 100644 --- a/src/applications/phortune/controller/PhortuneCartAcceptController.php +++ b/src/applications/phortune/controller/PhortuneCartAcceptController.php @@ -1,58 +1,52 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); // You must control the merchant to accept orders. $authority = $this->loadMerchantAuthority(); if (!$authority) { return new Aphront404Response(); } $cart = id(new PhortuneCartQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->withMerchantPHIDs(array($authority->getPHID())) ->needPurchases(true) ->executeOne(); if (!$cart) { return new Aphront404Response(); } $cancel_uri = $cart->getDetailURI($authority); if ($cart->getStatus() !== PhortuneCart::STATUS_REVIEW) { return $this->newDialog() ->setTitle(pht('Order Not in Review')) ->appendParagraph( pht( 'This order does not need manual review, so you can not '. 'accept it.')) ->addCancelButton($cancel_uri); } if ($request->isFormPost()) { $cart->didReviewCart(); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } return $this->newDialog() ->setTitle(pht('Accept Order?')) ->appendParagraph( pht( 'This order has been flagged for manual review. You should review '. 'it carefully before accepting it.')) ->addCancelButton($cancel_uri) ->addSubmitButton(pht('Accept Order')); } } diff --git a/src/applications/phortune/controller/PhortuneCartCancelController.php b/src/applications/phortune/controller/PhortuneCartCancelController.php index 3aedceb6b0..c4a26c0d00 100644 --- a/src/applications/phortune/controller/PhortuneCartCancelController.php +++ b/src/applications/phortune/controller/PhortuneCartCancelController.php @@ -1,213 +1,206 @@ id = $data['id']; - $this->action = $data['action']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $action = $request->getURIData('action'); $authority = $this->loadMerchantAuthority(); $cart_query = id(new PhortuneCartQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needPurchases(true); if ($authority) { $cart_query->withMerchantPHIDs(array($authority->getPHID())); } $cart = $cart_query->executeOne(); if (!$cart) { return new Aphront404Response(); } - switch ($this->action) { + switch ($action) { case 'cancel': // You must be able to edit the account to cancel an order. PhabricatorPolicyFilter::requireCapability( $viewer, $cart->getAccount(), PhabricatorPolicyCapability::CAN_EDIT); $is_refund = false; break; case 'refund': // You must be able to control the merchant to refund an order. PhabricatorPolicyFilter::requireCapability( $viewer, $cart->getMerchant(), PhabricatorPolicyCapability::CAN_EDIT); $is_refund = true; break; default: return new Aphront404Response(); } $cancel_uri = $cart->getDetailURI($authority); $merchant = $cart->getMerchant(); try { if ($is_refund) { $title = pht('Unable to Refund Order'); $cart->assertCanRefundOrder(); } else { $title = pht('Unable to Cancel Order'); $cart->assertCanCancelOrder(); } } catch (Exception $ex) { return $this->newDialog() ->setTitle($title) ->appendChild($ex->getMessage()) ->addCancelButton($cancel_uri); } $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withCartPHIDs(array($cart->getPHID())) ->withStatuses( array( PhortuneCharge::STATUS_HOLD, PhortuneCharge::STATUS_CHARGED, )) ->execute(); $amounts = mpull($charges, 'getAmountAsCurrency'); $maximum = PhortuneCurrency::newFromList($amounts); $v_refund = $maximum->formatForDisplay(); $errors = array(); $e_refund = true; if ($request->isFormPost()) { if ($is_refund) { try { $refund = PhortuneCurrency::newFromUserInput( $viewer, $request->getStr('refund')); $refund->assertInRange('0.00 USD', $maximum->formatForDisplay()); } catch (Exception $ex) { $errors[] = $ex->getMessage(); $e_refund = pht('Invalid'); } } else { $refund = $maximum; } if (!$errors) { $charges = msort($charges, 'getID'); $charges = array_reverse($charges); if ($charges) { $providers = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) ->withPHIDs(mpull($charges, 'getProviderPHID')) ->execute(); $providers = mpull($providers, null, 'getPHID'); } else { $providers = array(); } foreach ($charges as $charge) { $refundable = $charge->getAmountRefundableAsCurrency(); if (!$refundable->isPositive()) { // This charge is a refund, or has already been fully refunded. continue; } if ($refund->isGreaterThan($refundable)) { $refund_amount = $refundable; } else { $refund_amount = $refund; } $provider_config = idx($providers, $charge->getProviderPHID()); if (!$provider_config) { throw new Exception(pht('Unable to load provider for charge!')); } $provider = $provider_config->buildProvider(); $refund_charge = $cart->willRefundCharge( $viewer, $provider, $charge, $refund_amount); $refunded = false; try { $provider->refundCharge($charge, $refund_charge); $refunded = true; } catch (Exception $ex) { phlog($ex); $cart->didFailRefund($charge, $refund_charge); } if ($refunded) { $cart->didRefundCharge($charge, $refund_charge); $refund = $refund->subtract($refund_amount); } if (!$refund->isPositive()) { break; } } if ($refund->isPositive()) { throw new Exception(pht('Unable to refund some charges!')); } // TODO: If every HOLD and CHARGING transaction has been fully refunded // and we're in a HOLD, REVIEW, PURCHASING or CHARGED cart state we // probably need to kick the cart back to READY here (or maybe kill // it if it was in REVIEW)? return id(new AphrontRedirectResponse())->setURI($cancel_uri); } } if ($is_refund) { $title = pht('Refund Order?'); $body = pht('Really refund this order?'); $button = pht('Refund Order'); $cancel_text = pht('Cancel'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('refund') ->setLabel(pht('Amount')) ->setError($e_refund) ->setValue($v_refund)); $form = $form->buildLayoutView(); } else { $title = pht('Cancel Order?'); $body = pht('Really cancel this order? Any payment will be refunded.'); $button = pht('Cancel Order'); // Don't give the user a "Cancel" button in response to a "Cancel?" // prompt, as it's confusing. $cancel_text = pht('Do Not Cancel Order'); $form = null; } return $this->newDialog() ->setTitle($title) ->setErrors($errors) ->appendChild($body) ->appendChild($form) ->addSubmitButton($button) ->addCancelButton($cancel_uri, $cancel_text); } } diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php index 0d8e55f5c3..e22a9521fd 100644 --- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -1,230 +1,224 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $cart = id(new PhortuneCartQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->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')) ->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(); $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(), )); $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 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')) ->appendChild($form) ->appendChild($provider_form); $description_box = $this->renderCartDescription($cart); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Checkout')); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $cart_box, $description_box, $payment_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneCartUpdateController.php b/src/applications/phortune/controller/PhortuneCartUpdateController.php index ea571ccf8a..3d49611d2d 100644 --- a/src/applications/phortune/controller/PhortuneCartUpdateController.php +++ b/src/applications/phortune/controller/PhortuneCartUpdateController.php @@ -1,72 +1,66 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $authority = $this->loadMerchantAuthority(); $cart_query = id(new PhortuneCartQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needPurchases(true); if ($authority) { $cart_query->withMerchantPHIDs(array($authority->getPHID())); } $cart = $cart_query->executeOne(); if (!$cart) { return new Aphront404Response(); } $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withCartPHIDs(array($cart->getPHID())) ->needCarts(true) ->withStatuses( array( PhortuneCharge::STATUS_HOLD, PhortuneCharge::STATUS_CHARGED, )) ->execute(); if ($charges) { $providers = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) ->withPHIDs(mpull($charges, 'getProviderPHID')) ->execute(); $providers = mpull($providers, null, 'getPHID'); } else { $providers = array(); } foreach ($charges as $charge) { if ($charge->isRefund()) { // Don't update refunds. continue; } $provider_config = idx($providers, $charge->getProviderPHID()); if (!$provider_config) { throw new Exception(pht('Unable to load provider for charge!')); } $provider = $provider_config->buildProvider(); $provider->updateCharge($charge); } return id(new AphrontRedirectResponse()) ->setURI($cart->getDetailURI($authority)); } } diff --git a/src/applications/phortune/controller/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php index 9e133002f5..f4eb0d9612 100644 --- a/src/applications/phortune/controller/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/PhortuneCartViewController.php @@ -1,321 +1,315 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $authority = $this->loadMerchantAuthority(); $query = id(new PhortuneCartQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needPurchases(true); if ($authority) { $query->withMerchantPHIDs(array($authority->getPHID())); } $cart = $query->executeOne(); if (!$cart) { return new Aphront404Response(); } $cart_table = $this->buildCartContentTable($cart); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $cart, PhabricatorPolicyCapability::CAN_EDIT); $errors = array(); $error_view = null; $resume_uri = null; switch ($cart->getStatus()) { case PhortuneCart::STATUS_READY: if ($authority && $cart->getIsInvoice()) { // We arrived here by following the ad-hoc invoice workflow, and // are acting with merchant authority. $checkout_uri = PhabricatorEnv::getURI($cart->getCheckoutURI()); $invoice_message = array( pht( 'Manual invoices do not automatically notify recipients yet. '. 'Send the payer this checkout link:'), ' ', phutil_tag( 'a', array( 'href' => $checkout_uri, ), $checkout_uri), ); $error_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors(array($invoice_message)); } break; case PhortuneCart::STATUS_PURCHASING: if ($can_edit) { $resume_uri = $cart->getMetadataValue('provider.checkoutURI'); if ($resume_uri) { $errors[] = pht( 'The checkout process has been started, but not yet completed. '. 'You can continue checking out by clicking %s, or cancel the '. 'order, or contact the merchant for assistance.', phutil_tag('strong', array(), pht('Continue Checkout'))); } else { $errors[] = pht( 'The checkout process has been started, but an error occurred. '. 'You can cancel the order or contact the merchant for '. 'assistance.'); } } break; case PhortuneCart::STATUS_CHARGED: if ($can_edit) { $errors[] = pht( 'You have been charged, but processing could not be completed. '. 'You can cancel your order, or contact the merchant for '. 'assistance.'); } break; case PhortuneCart::STATUS_HOLD: if ($can_edit) { $errors[] = pht( 'Payment for this order is on hold. You can click %s to check '. 'for updates, cancel the order, or contact the merchant for '. 'assistance.', phutil_tag('strong', array(), pht('Update Status'))); } break; case PhortuneCart::STATUS_REVIEW: if ($authority) { $errors[] = pht( 'This order has been flagged for manual review. Review the order '. 'and choose %s to accept it or %s to reject it.', phutil_tag('strong', array(), pht('Accept Order')), phutil_tag('strong', array(), pht('Refund Order'))); } else if ($can_edit) { $errors[] = pht( 'This order requires manual processing and will complete once '. 'the merchant accepts it.'); } break; case PhortuneCart::STATUS_PURCHASED: $error_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('This purchase has been completed.')); break; } $properties = $this->buildPropertyListView($cart); $actions = $this->buildActionListView( $cart, $can_edit, $authority, $resume_uri); $properties->setActionList($actions); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader(pht('Order Detail')); if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) { $done_uri = $cart->getDoneURI(); if ($done_uri) { $header->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($done_uri) ->setIcon(id(new PHUIIconView()) ->setIconFont('fa-check-square green')) ->setText($cart->getDoneActionName())); } } $cart_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties) ->setTable($cart_table); if ($errors) { $cart_box->setFormErrors($errors); } else if ($error_view) { $cart_box->setInfoView($error_view); } $description = $this->renderCartDescription($cart); $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withCartPHIDs(array($cart->getPHID())) ->needCarts(true) ->execute(); $phids = array(); foreach ($charges as $charge) { $phids[] = $charge->getProviderPHID(); $phids[] = $charge->getCartPHID(); $phids[] = $charge->getMerchantPHID(); $phids[] = $charge->getPaymentMethodPHID(); } $handles = $this->loadViewerHandles($phids); $charges_table = id(new PhortuneChargeTableView()) ->setUser($viewer) ->setHandles($handles) ->setCharges($charges) ->setShowOrder(false); $charges = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Charges')) ->setTable($charges_table); $account = $cart->getAccount(); $crumbs = $this->buildApplicationCrumbs(); if ($authority) { $this->addMerchantCrumb($crumbs, $authority); } else { $this->addAccountCrumb($crumbs, $cart->getAccount()); } $crumbs->addTextCrumb(pht('Cart %d', $cart->getID())); $timeline = $this->buildTransactionTimeline( $cart, new PhortuneCartTransactionQuery()); $timeline ->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $cart_box, $description, $charges, $timeline, ), array( 'title' => pht('Cart'), )); } private function buildPropertyListView(PhortuneCart $cart) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($cart); $handles = $this->loadViewerHandles( array( $cart->getAccountPHID(), $cart->getAuthorPHID(), $cart->getMerchantPHID(), )); $view->addProperty( pht('Order Name'), $cart->getName()); $view->addProperty( pht('Account'), $handles[$cart->getAccountPHID()]->renderLink()); $view->addProperty( pht('Authorized By'), $handles[$cart->getAuthorPHID()]->renderLink()); $view->addProperty( pht('Merchant'), $handles[$cart->getMerchantPHID()]->renderLink()); $view->addProperty( pht('Status'), PhortuneCart::getNameForStatus($cart->getStatus())); $view->addProperty( pht('Updated'), phabricator_datetime($cart->getDateModified(), $viewer)); return $view; } private function buildActionListView( PhortuneCart $cart, $can_edit, $authority, $resume_uri) { $viewer = $this->getRequest()->getUser(); $id = $cart->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($cart); $can_cancel = ($can_edit && $cart->canCancelOrder()); if ($authority) { $prefix = 'merchant/'.$authority->getID().'/'; } else { $prefix = ''; } $cancel_uri = $this->getApplicationURI("{$prefix}cart/{$id}/cancel/"); $refund_uri = $this->getApplicationURI("{$prefix}cart/{$id}/refund/"); $update_uri = $this->getApplicationURI("{$prefix}cart/{$id}/update/"); $accept_uri = $this->getApplicationURI("{$prefix}cart/{$id}/accept/"); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Cancel Order')) ->setIcon('fa-times') ->setDisabled(!$can_cancel) ->setWorkflow(true) ->setHref($cancel_uri)); if ($authority) { if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Accept Order')) ->setIcon('fa-check') ->setWorkflow(true) ->setHref($accept_uri)); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Refund Order')) ->setIcon('fa-reply') ->setWorkflow(true) ->setHref($refund_uri)); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Update Status')) ->setIcon('fa-refresh') ->setHref($update_uri)); if ($can_edit && $resume_uri) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Continue Checkout')) ->setIcon('fa-shopping-cart') ->setHref($resume_uri)); } return $view; } } diff --git a/src/applications/phortune/controller/PhortuneChargeListController.php b/src/applications/phortune/controller/PhortuneChargeListController.php index 85a5a0cfb2..b8edb92507 100644 --- a/src/applications/phortune/controller/PhortuneChargeListController.php +++ b/src/applications/phortune/controller/PhortuneChargeListController.php @@ -1,81 +1,74 @@ accountID = idx($data, 'accountID'); - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); + $account_id = $request->getURIData('accountID'); $engine = new PhortuneChargeSearchEngine(); - if ($this->accountID) { + if ($account_id) { $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) - ->withIDs(array($this->accountID)) + ->withIDs(array($account_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return new Aphront404Response(); } $this->account = $account; $engine->setAccount($account); } else { return new Aphront404Response(); } $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine($engine) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView() { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhortuneChargeSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $account = $this->account; if ($account) { $id = $account->getID(); $crumbs->addTextCrumb( $account->getName(), $this->getApplicationURI("{$id}/")); $crumbs->addTextCrumb( pht('Charges'), $this->getApplicationURI("{$id}/charge/")); } return $crumbs; } } diff --git a/src/applications/phortune/controller/PhortuneLandingController.php b/src/applications/phortune/controller/PhortuneLandingController.php index ddff75f9ec..2a019c5df9 100644 --- a/src/applications/phortune/controller/PhortuneLandingController.php +++ b/src/applications/phortune/controller/PhortuneLandingController.php @@ -1,31 +1,30 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $accounts = id(new PhortuneAccountQuery()) - ->setViewer($user) - ->withMemberPHIDs(array($user->getPHID())) + ->setViewer($viewer) + ->withMemberPHIDs(array($viewer->getPHID())) ->execute(); if (!$accounts) { $account = PhortuneAccount::createNewAccount( - $user, + $viewer, PhabricatorContentSource::newFromRequest($request)); $accounts = array($account); } if (count($accounts) == 1) { $account = head($accounts); $next_uri = $this->getApplicationURI($account->getID().'/'); } else { $next_uri = $this->getApplicationURI('account/'); } return id(new AphrontRedirectResponse())->setURI($next_uri); } } diff --git a/src/applications/phortune/controller/PhortuneMerchantEditController.php b/src/applications/phortune/controller/PhortuneMerchantEditController.php index adb6e39c44..d3b396ddb0 100644 --- a/src/applications/phortune/controller/PhortuneMerchantEditController.php +++ b/src/applications/phortune/controller/PhortuneMerchantEditController.php @@ -1,179 +1,173 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { + if ($id) { $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $is_new = false; } else { $this->requireApplicationCapability( PhortuneMerchantCapability::CAPABILITY); $merchant = PhortuneMerchant::initializeNewMerchant($viewer); $merchant->attachMemberPHIDs(array($viewer->getPHID())); $is_new = true; } if ($is_new) { $title = pht('Create Merchant'); $button_text = pht('Create Merchant'); $cancel_uri = $this->getApplicationURI('merchant/'); } else { $title = pht( 'Edit Merchant %d %s', $merchant->getID(), $merchant->getName()); $button_text = pht('Save Changes'); $cancel_uri = $this->getApplicationURI( '/merchant/'.$merchant->getID().'/'); } $e_name = true; $v_name = $merchant->getName(); $v_desc = $merchant->getDescription(); $v_members = $merchant->getMemberPHIDs(); $e_members = null; $validation_exception = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('desc'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_members = $request->getArr('memberPHIDs'); $type_name = PhortuneMerchantTransaction::TYPE_NAME; $type_desc = PhortuneMerchantTransaction::TYPE_DESCRIPTION; $type_edge = PhabricatorTransactions::TYPE_EDGE; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $edge_members = PhortuneMerchantHasMemberEdgeType::EDGECONST; $xactions = array(); $xactions[] = id(new PhortuneMerchantTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new PhortuneMerchantTransaction()) ->setTransactionType($type_desc) ->setNewValue($v_desc); $xactions[] = id(new PhortuneMerchantTransaction()) ->setTransactionType($type_view) ->setNewValue($v_view); $xactions[] = id(new PhortuneMerchantTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_members) ->setNewValue( array( '=' => array_fuse($v_members), )); $editor = id(new PhortuneMerchantEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($merchant, $xactions); $id = $merchant->getID(); $merchant_uri = $this->getApplicationURI("merchant/{$id}/"); return id(new AphrontRedirectResponse())->setURI($merchant_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); $e_mbmers = $ex->getShortMessage($type_edge); $merchant->setViewPolicy($v_view); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($merchant) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('desc') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setLabel(pht('Members')) ->setName('memberPHIDs') ->setValue($v_members) ->setError($e_members)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($merchant) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button_text) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $crumbs->addTextCrumb(pht('Create Merchant')); } else { $crumbs->addTextCrumb( pht('Merchant %d', $merchant->getID()), $this->getApplicationURI('/merchant/'.$merchant->getID().'/')); $crumbs->addTextCrumb(pht('Edit')); } $box = id(new PHUIObjectBoxView()) ->setValidationException($validation_exception) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneMerchantListController.php b/src/applications/phortune/controller/PhortuneMerchantListController.php index 6849393de7..48fb02195b 100644 --- a/src/applications/phortune/controller/PhortuneMerchantListController.php +++ b/src/applications/phortune/controller/PhortuneMerchantListController.php @@ -1,57 +1,54 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhortuneMerchantSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView() { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhortuneMerchantSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $can_create = $this->hasApplicationCapability( PhortuneMerchantCapability::CAPABILITY); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Merchant')) ->setHref($this->getApplicationURI('merchant/edit/')) ->setIcon('fa-plus-square') ->setWorkflow(!$can_create) ->setDisabled(!$can_create)); return $crumbs; } } diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php index 2ca9f70c80..4b515c44c5 100644 --- a/src/applications/phortune/controller/PhortuneMerchantViewController.php +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -1,299 +1,293 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($merchant->getName()); $title = pht( 'Merchant %d %s', $merchant->getID(), $merchant->getName()); $header = id(new PHUIHeaderView()) ->setHeader($merchant->getName()) ->setUser($viewer) ->setPolicyObject($merchant); $providers = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) ->withMerchantPHIDs(array($merchant->getPHID())) ->execute(); $properties = $this->buildPropertyListView($merchant, $providers); $actions = $this->buildActionListView($merchant); $properties->setActionList($actions); $provider_list = $this->buildProviderList( $merchant, $providers); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $timeline = $this->buildTransactionTimeline( $merchant, new PhortuneMerchantTransactionQuery()); $timeline->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $box, $provider_list, $timeline, ), array( 'title' => $title, )); } private function buildPropertyListView( PhortuneMerchant $merchant, array $providers) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($merchant); $status_view = new PHUIStatusListView(); $have_any = false; $any_test = false; foreach ($providers as $provider_config) { $provider = $provider_config->buildProvider(); if ($provider->isEnabled()) { $have_any = true; } if (!$provider->isAcceptingLivePayments()) { $any_test = true; } } if ($have_any) { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Accepts Payments')) ->setNote(pht('This merchant can accept payments.'))); if ($any_test) { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') ->setTarget(pht('Test Mode')) ->setNote(pht('This merchant is accepting test payments.'))); } else { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Live Mode')) ->setNote(pht('This merchant is accepting live payments.'))); } } else if ($providers) { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_REJECT, 'red') ->setTarget(pht('No Enabled Providers')) ->setNote( pht( 'All of the payment providers for this merchant are '. 'disabled.'))); } else { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') ->setTarget(pht('No Providers')) ->setNote( pht( 'This merchant does not have any payment providers configured '. 'yet, so it can not accept payments. Add a provider.'))); } $view->addProperty(pht('Status'), $status_view); $view->addProperty( pht('Members'), $viewer->renderHandleList($merchant->getMemberPHIDs())); $view->invokeWillRenderEvent(); $description = $merchant->getDescription(); if (strlen($description)) { $description = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($description), 'default', $viewer); $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } return $view; } private function buildActionListView(PhortuneMerchant $merchant) { $viewer = $this->getRequest()->getUser(); $id = $merchant->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $merchant, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($merchant); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Merchant')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($this->getApplicationURI("merchant/edit/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Orders')) ->setIcon('fa-shopping-cart') ->setHref($this->getApplicationURI("merchant/orders/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Subscriptions')) ->setIcon('fa-moon-o') ->setHref($this->getApplicationURI("merchant/{$id}/subscription/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('New Invoice')) ->setIcon('fa-fax') ->setHref($this->getApplicationURI("merchant/{$id}/invoice/new/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); return $view; } private function buildProviderList( PhortuneMerchant $merchant, array $providers) { $viewer = $this->getRequest()->getUser(); $id = $merchant->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $merchant, PhabricatorPolicyCapability::CAN_EDIT); $provider_list = id(new PHUIObjectItemListView()) ->setFlush(true) ->setNoDataString(pht('This merchant has no payment providers.')); foreach ($providers as $provider_config) { $provider = $provider_config->buildProvider(); $provider_id = $provider_config->getID(); $item = id(new PHUIObjectItemView()) ->setHeader($provider->getName()); if ($provider->isEnabled()) { if ($provider->isAcceptingLivePayments()) { $item->setStatusIcon('fa-check green'); } else { $item->setStatusIcon('fa-warning yellow'); $item->addIcon('fa-exclamation-triangle', pht('Test Mode')); } $item->addAttribute($provider->getConfigureProvidesDescription()); } else { // Don't show disabled providers to users who can't manage the merchant // account. if (!$can_edit) { continue; } $item->setDisabled(true); $item->addAttribute( phutil_tag('em', array(), pht('This payment provider is disabled.'))); } if ($can_edit) { $edit_uri = $this->getApplicationURI( "/provider/edit/{$provider_id}/"); $disable_uri = $this->getApplicationURI( "/provider/disable/{$provider_id}/"); if ($provider->isEnabled()) { $disable_icon = 'fa-times'; $disable_name = pht('Disable'); } else { $disable_icon = 'fa-check'; $disable_name = pht('Enable'); } $item->addAction( id(new PHUIListItemView()) ->setIcon($disable_icon) ->setHref($disable_uri) ->setName($disable_name) ->setWorkflow(true)); $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-pencil') ->setHref($edit_uri) ->setName(pht('Edit'))); } $provider_list->addItem($item); } $add_action = id(new PHUIButtonView()) ->setTag('a') ->setHref($this->getApplicationURI('provider/edit/?merchantID='.$id)) ->setText(pht('Add Payment Provider')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setIcon(id(new PHUIIconView())->setIconFont('fa-plus')); $header = id(new PHUIHeaderView()) ->setHeader(pht('Payment Providers')) ->addActionLink($add_action); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setObjectList($provider_list); } } diff --git a/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php index 6b7e55bb54..dd783d4c51 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php @@ -1,276 +1,270 @@ accountID = $data['accountID']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $account_id = $request->getURIData('accountID'); $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) - ->withIDs(array($this->accountID)) + ->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(); 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); 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); } } if (!$errors) { $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 = $cancel_uri; } 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) ->setTitle(pht('Error Adding Payment Method')) ->appendChild(id(new PHUIInfoView())->setErrors($errors)) ->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($provider->getPaymentMethodDescription()) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Add Payment Method')); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $provider->getPaymentMethodDescription(), )); } 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/PhortunePaymentMethodDisableController.php b/src/applications/phortune/controller/PhortunePaymentMethodDisableController.php index 8f5124bdb4..4b8fb76010 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodDisableController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodDisableController.php @@ -1,64 +1,58 @@ methodID = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $method_id = $request->getURIData('methodID'); $method = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) - ->withIDs(array($this->methodID)) + ->withIDs(array($method_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$method) { return new Aphront404Response(); } if ($method->getStatus() == PhortunePaymentMethod::STATUS_DISABLED) { return new Aphront400Response(); } $account = $method->getAccount(); $account_uri = $this->getApplicationURI($account->getID().'/'); if ($request->isFormPost()) { // TODO: ApplicationTransactions! $method ->setStatus(PhortunePaymentMethod::STATUS_DISABLED) ->save(); return id(new AphrontRedirectResponse())->setURI($account_uri); } return $this->newDialog() ->setTitle(pht('Disable Payment Method?')) ->setShortTitle(pht('Disable Payment Method')) ->appendParagraph( pht( 'Disable the payment method "%s"?', phutil_tag( 'strong', array(), $method->getFullDisplayName()))) ->appendParagraph( pht( 'You will no longer be able to make payments using this payment '. 'method. Disabled payment methods can not be reactivated.')) ->addCancelButton($account_uri) ->addSubmitButton(pht('Disable Payment Method')); } } diff --git a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php index 2db514a1cf..27edd72a59 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php @@ -1,85 +1,79 @@ methodID = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $method_id = $request->getURIData('id'); $method = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) - ->withIDs(array($this->methodID)) + ->withIDs(array($method_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$method) { return new Aphront404Response(); } $account = $method->getAccount(); $account_uri = $this->getApplicationURI($account->getID().'/'); if ($request->isFormPost()) { $name = $request->getStr('name'); // TODO: Use ApplicationTransactions $method->setName($name); $method->save(); return id(new AphrontRedirectResponse())->setURI($account_uri); } $provider = $method->buildPaymentProvider(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($method->getName())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Details')) ->setValue($method->getSummary())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Expires')) ->setValue($method->getDisplayExpires())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($account_uri) ->setValue(pht('Save Changes'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Payment Method')) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($account->getName(), $account_uri); $crumbs->addTextCrumb($method->getDisplayName()); $crumbs->addTextCrumb(pht('Edit')); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => pht('Edit Payment Method'), )); } } diff --git a/src/applications/phortune/controller/PhortuneProductListController.php b/src/applications/phortune/controller/PhortuneProductListController.php index 8a1181b33e..a82effa6d4 100644 --- a/src/applications/phortune/controller/PhortuneProductListController.php +++ b/src/applications/phortune/controller/PhortuneProductListController.php @@ -1,59 +1,58 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $query = id(new PhortuneProductQuery()) - ->setViewer($user); + ->setViewer($viewer); $products = $query->executeWithCursorPager($pager); $title = pht('Product List'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Products'), $this->getApplicationURI('product/')); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Product')) ->setHref($this->getApplicationURI('product/edit/')) ->setIcon('fa-plus-square')); $product_list = id(new PHUIObjectItemListView()) - ->setUser($user) + ->setUser($viewer) ->setNoDataString(pht('No products.')); foreach ($products as $product) { $view_uri = $this->getApplicationURI( 'product/view/'.$product->getID().'/'); $price = $product->getPriceAsCurrency(); $item = id(new PHUIObjectItemView()) ->setObjectName($product->getID()) ->setHeader($product->getProductName()) ->setHref($view_uri) ->addAttribute($price->formatForDisplay()); $product_list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $product_list, $pager, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneProductViewController.php b/src/applications/phortune/controller/PhortuneProductViewController.php index 02426f07ca..73d120bfd9 100644 --- a/src/applications/phortune/controller/PhortuneProductViewController.php +++ b/src/applications/phortune/controller/PhortuneProductViewController.php @@ -1,63 +1,57 @@ productID = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $product = id(new PhortuneProductQuery()) - ->setViewer($user) - ->withIDs(array($this->productID)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$product) { return new Aphront404Response(); } $title = pht('Product: %s', $product->getProductName()); $header = id(new PHUIHeaderView()) ->setHeader($product->getProductName()); $edit_uri = $this->getApplicationURI('product/edit/'.$product->getID().'/'); $actions = id(new PhabricatorActionListView()) - ->setUser($user) + ->setUser($viewer) ->setObjectURI($request->getRequestURI()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Products'), $this->getApplicationURI('product/')); $crumbs->addTextCrumb( pht('#%d', $product->getID()), $request->getRequestURI()); $properties = id(new PHUIPropertyListView()) - ->setUser($user) + ->setUser($viewer) ->setActionList($actions) ->addProperty( pht('Price'), $product->getPriceAsCurrency()->formatForDisplay()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneProviderActionController.php b/src/applications/phortune/controller/PhortuneProviderActionController.php index b856b67d97..52d5453f42 100644 --- a/src/applications/phortune/controller/PhortuneProviderActionController.php +++ b/src/applications/phortune/controller/PhortuneProviderActionController.php @@ -1,85 +1,80 @@ id = $data['id']; - $this->setAction($data['action']); - } - public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); + $this->setAction($request->getURIData('action')); $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$provider_config) { return new Aphront404Response(); } $provider = $provider_config->buildProvider(); if (!$provider->canRespondToControllerAction($this->getAction())) { return new Aphront404Response(); } $response = $provider->processControllerRequest($this, $request); if ($response instanceof AphrontResponse) { return $response; } return $this->buildApplicationPage( $response, array( 'title' => pht('Phortune'), )); } public function loadCart($id) { $request = $this->getRequest(); $viewer = $request->getUser(); return id(new PhortuneCartQuery()) ->setViewer($viewer) ->needPurchases(true) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); } public function loadActiveCharge(PhortuneCart $cart) { $request = $this->getRequest(); $viewer = $request->getUser(); return id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withCartPHIDs(array($cart->getPHID())) ->withStatuses( array( PhortuneCharge::STATUS_CHARGING, )) ->executeOne(); } } diff --git a/src/applications/phortune/controller/PhortuneProviderDisableController.php b/src/applications/phortune/controller/PhortuneProviderDisableController.php index e398cbc886..03236b54bc 100644 --- a/src/applications/phortune/controller/PhortuneProviderDisableController.php +++ b/src/applications/phortune/controller/PhortuneProviderDisableController.php @@ -1,76 +1,70 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$provider_config) { return new Aphront404Response(); } $merchant = $provider_config->getMerchant(); $merchant_id = $merchant->getID(); $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); $provider = $provider_config->buildProvider(); if ($request->isFormPost()) { $new_status = !$provider_config->getIsEnabled(); $xactions = array(); $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) ->setTransactionType( PhortunePaymentProviderConfigTransaction::TYPE_ENABLE) ->setNewValue($new_status); $editor = id(new PhortunePaymentProviderConfigEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $editor->applyTransactions($provider_config, $xactions); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } if ($provider_config->getIsEnabled()) { $title = pht('Disable Provider?'); $body = pht( 'If you disable this payment provider, users will no longer be able '. 'to use it to make new payments.'); $button = pht('Disable Provider'); } else { $title = pht('Enable Provider?'); $body = pht( 'If you enable this payment provider, users will be able to use it to '. 'make new payments.'); $button = pht('Enable Provider'); } return $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addSubmitButton($button) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/phortune/controller/PhortuneProviderEditController.php b/src/applications/phortune/controller/PhortuneProviderEditController.php index 3ed97b5a4b..f956c41d68 100644 --- a/src/applications/phortune/controller/PhortuneProviderEditController.php +++ b/src/applications/phortune/controller/PhortuneProviderEditController.php @@ -1,291 +1,285 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if ($this->id) { + if ($id) { $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$provider_config) { return new Aphront404Response(); } $is_new = false; $is_choose_type = false; $merchant = $provider_config->getMerchant(); $merchant_id = $merchant->getID(); $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); } else { $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->withIDs(array($request->getStr('merchantID'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $merchant_id = $merchant->getID(); $current_providers = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) ->withMerchantPHIDs(array($merchant->getPHID())) ->execute(); $current_map = mgroup($current_providers, 'getProviderClass'); $provider_config = PhortunePaymentProviderConfig::initializeNewProvider( $merchant); $is_new = true; $classes = PhortunePaymentProvider::getAllProviders(); $class = $request->getStr('class'); if (empty($classes[$class]) || isset($current_map[$class])) { return $this->processChooseClassRequest( $request, $merchant, $current_map); } $provider_config->setProviderClass($class); $cancel_uri = $this->getApplicationURI( 'provider/edit/?merchantID='.$merchant_id); } $provider = $provider_config->buildProvider(); if ($is_new) { $title = pht('Create Payment Provider'); $button_text = pht('Create Provider'); } else { $title = pht( 'Edit Payment Provider %d %s', $provider_config->getID(), $provider->getName()); $button_text = pht('Save Changes'); } $errors = array(); if ($request->isFormPost() && $request->getStr('edit')) { $form_values = $provider->readEditFormValuesFromRequest($request); list($errors, $issues, $xaction_values) = $provider->processEditForm( $request, $form_values); if (!$errors) { // Find any secret fields which we're about to set to "*******" // (indicating that the user did not edit the value) and remove them // from the list of properties to update (so we don't write "******" // to permanent configuration. $secrets = $provider->getAllConfigurableSecretProperties(); $secrets = array_fuse($secrets); foreach ($xaction_values as $key => $value) { if ($provider->isConfigurationSecret($value)) { unset($xaction_values[$key]); } } if ($provider->canRunConfigurationTest()) { $proxy = clone $provider; $proxy_config = clone $provider_config; $proxy_config->setMetadata( $xaction_values + $provider_config->getMetadata()); $proxy->setProviderConfig($proxy_config); try { $proxy->runConfigurationTest(); } catch (Exception $ex) { $errors[] = pht('Unable to connect to payment provider:'); $errors[] = $ex->getMessage(); } } if (!$errors) { $template = id(new PhortunePaymentProviderConfigTransaction()) ->setTransactionType( PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY); $xactions = array(); $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) ->setTransactionType( PhortunePaymentProviderConfigTransaction::TYPE_CREATE) ->setNewValue(true); foreach ($xaction_values as $key => $value) { $xactions[] = id(clone $template) ->setMetadataValue( PhortunePaymentProviderConfigTransaction::PROPERTY_KEY, $key) ->setNewValue($value); } $editor = id(new PhortunePaymentProviderConfigEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $editor->applyTransactions($provider_config, $xactions); $merchant_uri = $this->getApplicationURI( 'merchant/'.$merchant->getID().'/'); return id(new AphrontRedirectResponse())->setURI($merchant_uri); } } } else { $form_values = $provider->readEditFormValuesFromProviderConfig(); $issues = array(); } $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('merchantID', $merchant->getID()) ->addHiddenInput('class', $provider_config->getProviderClass()) ->addHiddenInput('edit', true) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Provider Type')) ->setValue($provider->getName())); $provider->extendEditForm($request, $form, $form_values, $issues); $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button_text) ->addCancelButton($cancel_uri)) ->appendChild( id(new AphrontFormDividerControl())) ->appendRemarkupInstructions( $provider->getConfigureInstructions()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($merchant->getName(), $cancel_uri); if ($is_new) { $crumbs->addTextCrumb(pht('Add Provider')); } else { $crumbs->addTextCrumb( pht('Edit Provider %d', $provider_config->getID())); } $box = id(new PHUIObjectBoxView()) ->setFormErrors($errors) ->setHeaderText($title) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } private function processChooseClassRequest( AphrontRequest $request, PhortuneMerchant $merchant, array $current_map) { $viewer = $request->getUser(); $providers = PhortunePaymentProvider::getAllProviders(); $v_class = null; $errors = array(); if ($request->isFormPost()) { $v_class = $request->getStr('class'); if (!isset($providers[$v_class])) { $errors[] = pht('You must select a valid provider type.'); } } $merchant_id = $merchant->getID(); $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); if (!$v_class) { $v_class = key($providers); } $panel_classes = id(new AphrontFormRadioButtonControl()) ->setName('class') ->setValue($v_class); $providers = msort($providers, 'getConfigureName'); foreach ($providers as $class => $provider) { $disabled = isset($current_map[$class]); if ($disabled) { $description = phutil_tag( 'em', array(), pht( 'This merchant already has a payment account configured '. 'with this provider.')); } else { $description = $provider->getConfigureDescription(); } $panel_classes->addButton( $class, $provider->getConfigureName(), $description, null, $disabled); } $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('merchantID', $merchant->getID()) ->appendRemarkupInstructions( pht('Choose the type of payment provider to add:')) ->appendChild($panel_classes) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue')) ->addCancelButton($cancel_uri)); $title = pht('Add Payment Provider'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($merchant->getName(), $cancel_uri); $crumbs->addTextCrumb($title); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneSubscriptionListController.php b/src/applications/phortune/controller/PhortuneSubscriptionListController.php index 4fbdb804c3..469960f4bc 100644 --- a/src/applications/phortune/controller/PhortuneSubscriptionListController.php +++ b/src/applications/phortune/controller/PhortuneSubscriptionListController.php @@ -1,107 +1,99 @@ merchantID = idx($data, 'merchantID'); - $this->accountID = idx($data, 'accountID'); - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $querykey = $request->getURIData('queryKey'); + $merchant_id = $request->getURIData('merchantID'); + $account_id = $request->getURIData('accountID'); $engine = new PhortuneSubscriptionSearchEngine(); - if ($this->merchantID) { + if ($merchant_id) { $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) - ->withIDs(array($this->merchantID)) + ->withIDs(array($merchant_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $this->merchant = $merchant; $viewer->grantAuthority($merchant); $engine->setMerchant($merchant); - } else if ($this->accountID) { + } else if ($account_id) { $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) - ->withIDs(array($this->accountID)) + ->withIDs(array($account_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return new Aphront404Response(); } $this->account = $account; $engine->setAccount($account); } else { return new Aphront404Response(); } $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine($engine) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView() { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhortuneSubscriptionSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $merchant = $this->merchant; if ($merchant) { $id = $merchant->getID(); $this->addMerchantCrumb($crumbs, $merchant); $crumbs->addTextCrumb( pht('Subscriptions'), $this->getApplicationURI("merchant/subscriptions/{$id}/")); } $account = $this->account; if ($account) { $id = $account->getID(); $this->addAccountCrumb($crumbs, $account); $crumbs->addTextCrumb( pht('Subscriptions'), $this->getApplicationURI("{$id}/subscription/")); } return $crumbs; } } diff --git a/src/applications/phortune/query/PhortuneChargeSearchEngine.php b/src/applications/phortune/query/PhortuneChargeSearchEngine.php index 598614d885..0d6c2cfd59 100644 --- a/src/applications/phortune/query/PhortuneChargeSearchEngine.php +++ b/src/applications/phortune/query/PhortuneChargeSearchEngine.php @@ -1,135 +1,135 @@ account = $account; return $this; } public function getAccount() { return $this->account; } public function getResultTypeDescription() { return pht('Phortune Charges'); } public function getApplicationClassName() { return 'PhabricatorPhortuneApplication'; } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhortuneChargeQuery()); $viewer = $this->requireViewer(); $account = $this->getAccount(); if ($account) { $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $account, PhabricatorPolicyCapability::CAN_EDIT); if (!$can_edit) { throw new Exception( pht( 'You can not query charges for an account you are not '. 'a member of.')); } $query->withAccountPHIDs(array($account->getPHID())); } else { $accounts = id(new PhortuneAccountQuery()) ->withMemberPHIDs(array($viewer->getPHID())) ->execute(); if ($accounts) { $query->withAccountPHIDs(mpull($accounts, 'getPHID')); } else { throw new Exception(pht('You have no accounts!')); } } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) {} protected function getURI($path) { $account = $this->getAccount(); if ($account) { return '/phortune/'.$account->getID().'/charge/'; } else { return '/phortune/charge/'.$path; } } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All Charges'), ); 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 $charges, PhabricatorSavedQuery $query) { $phids = array(); foreach ($charges as $charge) { $phids[] = $charge->getProviderPHID(); $phids[] = $charge->getCartPHID(); $phids[] = $charge->getMerchantPHID(); $phids[] = $charge->getPaymentMethodPHID(); } return $phids; } protected function renderResultList( array $charges, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($charges, 'PhortuneCharge'); $viewer = $this->requireViewer(); $table = id(new PhortuneChargeTableView()) ->setUser($viewer) ->setCharges($charges) ->setHandles($handles); $result = new PhabricatorApplicationSearchResultView(); $result->setTable($table); - return $table; + return $result; } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php index 560d4203a0..2c5a43687c 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php @@ -1,32 +1,26 @@ id = $data['id']; - } - - public function processRequest() { - $id = $this->id; + public function handleRequest(AphrontRequest $request) { + $id = $request->getURIData('id'); return $this->buildStandardPageResponse( phutil_tag( 'iframe', array( 'src' => '/xhpast/frameset/'.$id.'/', 'frameborder' => '0', 'style' => 'width: 100%; height: 800px;', '', )), array( 'title' => pht('XHPAST View'), )); } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php index a21bb30b68..de446b5e44 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php @@ -1,32 +1,26 @@ id = $data['id']; - } - - public function processRequest() { - $id = $this->id; + public function handleRequest(AphrontRequest $request) { + $id = $request->getURIData('id'); $response = new AphrontWebpageResponse(); $response->setFrameable(true); $response->setContent(phutil_tag( 'frameset', array('cols' => '33%, 34%, 33%'), array( phutil_tag('frame', array('src' => "/xhpast/input/{$id}/")), phutil_tag('frame', array('src' => "/xhpast/tree/{$id}/")), phutil_tag('frame', array('src' => "/xhpast/stream/{$id}/")), ))); return $response; } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewInputController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewInputController.php index 48ba6afc33..6e1fd87db7 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewInputController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewInputController.php @@ -1,10 +1,10 @@ getStorageTree()->getInput(); return $this->buildXHPASTViewPanelResponse($input); } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php index f5a79a4b28..dd9cf85433 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php @@ -1,57 +1,55 @@ getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); if ($request->isFormPost()) { $source = $request->getStr('source'); $future = PhutilXHPASTBinary::getParserFuture($source); $resolved = $future->resolve(); // This is just to let it throw exceptions if stuff is broken. $parse_tree = XHPASTTree::newFromDataAndResolvedExecFuture( $source, $resolved); list($err, $stdout, $stderr) = $resolved; $storage_tree = new PhabricatorXHPASTViewParseTree(); $storage_tree->setInput($source); $storage_tree->setStdout($stdout); - $storage_tree->setAuthorPHID($user->getPHID()); + $storage_tree->setAuthorPHID($viewer->getPHID()); $storage_tree->save(); return id(new AphrontRedirectResponse()) ->setURI('/xhpast/view/'.$storage_tree->getID().'/'); } $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Source')) ->setName('source') ->setValue("setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Parse'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Generate XHP AST')) ->setForm($form); return $this->buildApplicationPage( $form_box, array( 'title' => pht('XHPAST View'), )); } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php index 4a098f1591..3fe1046f10 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php @@ -1,33 +1,33 @@ getStorageTree(); $input = $storage->getInput(); $stdout = $storage->getStdout(); $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $input, array(0, $stdout, '')); $tokens = array(); foreach ($tree->getRawTokenStream() as $id => $token) { $seq = $id; $name = $token->getTypeName(); $title = pht('Token %s: %s', $seq, $name); $tokens[] = phutil_tag( 'span', array( 'title' => $title, 'class' => 'token', ), $token->getValue()); } return $this->buildXHPASTViewPanelResponse( phutil_implode_html('', $tokens)); } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewTreeController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewTreeController.php index 7da285f121..1b4eec6441 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewTreeController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewTreeController.php @@ -1,49 +1,49 @@ getStorageTree(); $input = $storage->getInput(); $stdout = $storage->getStdout(); $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $input, array(0, $stdout, '')); $tree = phutil_tag('ul', array(), $this->buildTree($tree->getRootNode())); return $this->buildXHPASTViewPanelResponse($tree); } protected function buildTree($root) { try { $name = $root->getTypeName(); $title = $root->getDescription(); } catch (Exception $ex) { $name = '???'; $title = '???'; } $tree = array(); $tree[] = phutil_tag( 'li', array(), phutil_tag( 'span', array( 'title' => $title, ), $name)); foreach ($root->getChildren() as $child) { $tree[] = phutil_tag('ul', array(), $this->buildTree($child)); } return phutil_implode_html("\n", $tree); } } diff --git a/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php b/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php index 48f72d57db..575d4b3dd8 100644 --- a/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php +++ b/src/applications/phriction/herald/PhrictionDocumentHeraldAdapter.php @@ -1,80 +1,55 @@ document = $this->newObject(); } protected function newObject() { return new PhrictionDocument(); } public function getObject() { return $this->document; } public function setDocument(PhrictionDocument $document) { $this->document = $document; return $this; } public function getDocument() { return $this->document; } public function getAdapterContentName() { return pht('Phriction Documents'); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } - public function getActions($rule_type) { - switch ($rule_type) { - case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return array_merge( - array( - self::ACTION_ADD_CC, - self::ACTION_REMOVE_CC, - self::ACTION_EMAIL, - self::ACTION_FLAG, - self::ACTION_NOTHING, - ), - parent::getActions($rule_type)); - } - } - - public function getHeraldName() { return pht('Wiki Document %d', $this->getDocument()->getID()); } } diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php index 999580e90b..11e90dc274 100644 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -1,88 +1,113 @@ [1-9]\d*)' => 'PonderQuestionViewController', + '/Q(?P[1-9]\d*)' + => 'PonderQuestionViewController', '/ponder/' => array( - '(?:query/(?P[^/]+)/)?' => 'PonderQuestionListController', - 'answer/add/' => 'PonderAnswerSaveController', - 'answer/edit/(?P\d+)/' => 'PonderAnswerEditController', - 'answer/comment/(?P\d+)/' => 'PonderAnswerCommentController', - 'answer/history/(?P\d+)/' => 'PonderAnswerHistoryController', - 'question/edit/(?:(?P\d+)/)?' => 'PonderQuestionEditController', - 'question/comment/(?P\d+)/' => 'PonderQuestionCommentController', - 'question/history/(?P\d+)/' => 'PonderQuestionHistoryController', - 'preview/' => 'PhabricatorMarkupPreviewController', + '(?:query/(?P[^/]+)/)?' + => 'PonderQuestionListController', + 'answer/add/' + => 'PonderAnswerSaveController', + 'answer/edit/(?P\d+)/' + => 'PonderAnswerEditController', + 'answer/comment/(?P\d+)/' + => 'PonderAnswerCommentController', + 'answer/history/(?P\d+)/' + => 'PonderAnswerHistoryController', + 'question/edit/(?:(?P\d+)/)?' + => 'PonderQuestionEditController', + 'question/create/' + => 'PonderQuestionEditController', + 'question/comment/(?P\d+)/' + => 'PonderQuestionCommentController', + 'question/history/(?P\d+)/' + => 'PonderQuestionHistoryController', + 'preview/' + => 'PhabricatorMarkupPreviewController', 'question/(?Popen|close)/(?P[1-9]\d*)/' => 'PonderQuestionStatusController', 'vote/' => 'PonderVoteSaveController', ), ); } public function getMailCommandObjects() { return array( 'question' => array( 'name' => pht('Email Commands: Questions'), 'header' => pht('Interacting with Ponder Questions'), 'object' => new PonderQuestion(), 'summary' => pht( 'This page documents the commands you can use to interact with '. 'questions in Ponder.'), ), ); } + protected function getCustomCapabilities() { + return array( + PonderQuestionDefaultViewCapability::CAPABILITY => array( + 'template' => PonderQuestionPHIDType::TYPECONST, + 'capability' => PhabricatorPolicyCapability::CAN_VIEW, + ), + PonderQuestionDefaultEditCapability::CAPABILITY => array( + 'template' => PonderQuestionPHIDType::TYPECONST, + 'capability' => PhabricatorPolicyCapability::CAN_EDIT, + ), + ); + } + public function getApplicationSearchDocumentTypes() { return array( PonderQuestionPHIDType::TYPECONST, ); } } diff --git a/src/applications/ponder/capability/PonderQuestionDefaultEditCapability.php b/src/applications/ponder/capability/PonderQuestionDefaultEditCapability.php new file mode 100644 index 0000000000..2f9ca73b0f --- /dev/null +++ b/src/applications/ponder/capability/PonderQuestionDefaultEditCapability.php @@ -0,0 +1,12 @@ +getViewer(); $id = $request->getURIData('id'); $answer = id(new PonderAnswerQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$answer) { return new Aphront404Response(); } $timeline = $this->buildTransactionTimeline( $answer, new PonderAnswerTransactionQuery()); $timeline->setShouldTerminate(true); $qid = $answer->getQuestion()->getID(); $aid = $answer->getID(); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setBorder(true); $crumbs->addTextCrumb("Q{$qid}", "/Q{$qid}"); $crumbs->addTextCrumb("A{$aid}", "/Q{$qid}#{$aid}"); $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Answer History'), )); } } diff --git a/src/applications/ponder/controller/PonderController.php b/src/applications/ponder/controller/PonderController.php index 30bd412504..a14d70c773 100644 --- a/src/applications/ponder/controller/PonderController.php +++ b/src/applications/ponder/controller/PonderController.php @@ -1,32 +1,41 @@ getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + if ($for_app) { + $nav->addFilter('question/create/', pht('Ask Question')); + } + id(new PonderQuestionSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } + public function buildApplicationMenu() { + return $this->buildSideNavView($for_app = true)->getMenu(); + } + protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); + $href = $this->getApplicationURI('question/create/'); $crumbs ->addAction( id(new PHUIListItemView()) - ->setName(pht('Create Question')) - ->setHref('/ponder/question/edit/') + ->setName(pht('Ask Question')) + ->setHref($href) ->setIcon('fa-plus-square')); return $crumbs; } } diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php index 36c723445d..cf8b5f9de1 100644 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -1,145 +1,178 @@ getViewer(); + $viewer = $request->getViewer(); $id = $request->getURIData('id'); if ($id) { $question = id(new PonderQuestionQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$question) { return new Aphront404Response(); } $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $question->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } else { - $question = id(new PonderQuestion()) - ->setStatus(PonderQuestionStatus::STATUS_OPEN) - ->setAuthorPHID($user->getPHID()) - ->setVoteCount(0) - ->setAnswerCount(0) - ->setHeat(0.0); + $question = PonderQuestion::initializeNewQuestion($viewer); $v_projects = array(); } $v_title = $question->getTitle(); $v_content = $question->getContent(); + $v_view = $question->getViewPolicy(); + $v_edit = $question->getEditPolicy(); + $v_space = $question->getSpacePHID(); $errors = array(); $e_title = true; if ($request->isFormPost()) { $v_title = $request->getStr('title'); $v_content = $request->getStr('content'); $v_projects = $request->getArr('projects'); + $v_view = $request->getStr('viewPolicy'); + $v_edit = $request->getStr('editPolicy'); + $v_space = $request->getStr('spacePHID'); $len = phutil_utf8_strlen($v_title); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } if (!$errors) { $template = id(new PonderQuestionTransaction()); $xactions = array(); $xactions[] = id(clone $template) ->setTransactionType(PonderQuestionTransaction::TYPE_TITLE) ->setNewValue($v_title); $xactions[] = id(clone $template) ->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT) ->setNewValue($v_content); + $xactions[] = id(clone $template) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($v_view); + + $xactions[] = id(clone $template) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($v_edit); + + $xactions[] = id(clone $template) + ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) + ->setNewValue($v_space); + $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PonderQuestionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PonderQuestionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $editor->applyTransactions($question, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/Q'.$question->getID()); } } + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($question) + ->execute(); + $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Question')) ->setName('title') ->setValue($v_title) ->setError($e_title)) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setUser($user) + ->setUser($viewer) ->setName('content') ->setID('content') ->setValue($v_content) ->setLabel(pht('Description')) - ->setUser($user)); + ->setUser($viewer)) + ->appendControl( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($question) + ->setSpacePHID($v_space) + ->setPolicies($policies) + ->setValue($v_view) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) + ->appendControl( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($question) + ->setPolicies($policies) + ->setValue($v_edit) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)); $form->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())); - $form ->appendChild( + $form->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Ask Away!'))); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Question Preview')) ->setControlID('content') ->setPreviewURI($this->getApplicationURI('preview/')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Ask New Question')) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $id = $question->getID(); if ($id) { $crumbs->addTextCrumb("Q{$id}", "/Q{$id}"); $crumbs->addTextCrumb(pht('Edit')); } else { $crumbs->addTextCrumb(pht('Ask Question')); } return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => pht('Ask New Question'), )); } } diff --git a/src/applications/ponder/controller/PonderQuestionHistoryController.php b/src/applications/ponder/controller/PonderQuestionHistoryController.php index 3d62a7fcb6..91ded8c094 100644 --- a/src/applications/ponder/controller/PonderQuestionHistoryController.php +++ b/src/applications/ponder/controller/PonderQuestionHistoryController.php @@ -1,38 +1,39 @@ getViewer(); $id = $request->getURIData('id'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$question) { return new Aphront404Response(); } $timeline = $this->buildTransactionTimeline( $question, new PonderQuestionTransactionQuery()); $timeline->setShouldTerminate(true); $qid = $question->getID(); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setBorder(true); $crumbs->addTextCrumb("Q{$qid}", "/Q{$qid}"); $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Question History'), )); } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index d2b5a53826..51f165b408 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,398 +1,403 @@ getViewer(); + $viewer = $request->getViewer(); $id = $request->getURIData('id'); $question = id(new PonderQuestionQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($id)) ->needAnswers(true) ->needViewerVotes(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } - $question->attachVotes($user->getPHID()); + $question->attachVotes($viewer->getPHID()); $question_xactions = $this->buildQuestionTransactions($question); $answers = $this->buildAnswers($question->getAnswers()); $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); - if (isset($authors[$user->getPHID()])) { + if (isset($authors[$viewer->getPHID()])) { $answer_add_panel = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild( pht( 'You have already answered this question. You can not answer '. 'twice, but you can edit your existing answer.')); } else { $answer_add_panel = new PonderAddAnswerView(); $answer_add_panel ->setQuestion($question) - ->setUser($user) + ->setUser($viewer) ->setActionURI('/ponder/answer/add/'); } - $header = id(new PHUIHeaderView()) - ->setHeader($question->getTitle()); + $header = new PHUIHeaderView(); + $header->setHeader($question->getTitle()); + $header->setUser($viewer); + $header->setPolicyObject($question); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $header->setStatus('fa-square-o', 'bluegrey', pht('Open')); } else { $header->setStatus('fa-check-square-o', 'dark', pht('Closed')); } $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); - return $this->buildApplicationPage( + $ponder_view = phutil_tag( + 'div', + array( + 'class' => 'ponder-question-view', + ), array( $crumbs, $object_box, $question_xactions, $answers, $answer_add_panel, + )); + + return $this->buildApplicationPage( + array( + $ponder_view, ), array( 'title' => 'Q'.$question->getID().' '.$question->getTitle(), 'pageObjects' => array_merge( array($question->getPHID()), mpull($question->getAnswers(), 'getPHID')), )); } private function buildActionListView(PonderQuestion $question) { + $viewer = $this->getViewer(); $request = $this->getRequest(); - $viewer = $request->getUser(); - $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) - ->setUser($request->getUser()) + ->setUser($viewer) ->setObject($question) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Question')) ->setHref($this->getApplicationURI("/question/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-check-square-o'; $href = 'close'; } else { $name = pht('Reopen Question'); $icon = 'fa-square-o'; $href = 'open'; } $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) ->setRenderAsForm($can_edit) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit) ->setHref($this->getApplicationURI("/question/{$href}/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); return $view; } private function buildPropertyListView( PonderQuestion $question, PhabricatorActionListView $actions) { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) ->setActionList($actions); - $view->addProperty( - pht('Status'), - PonderQuestionStatus::getQuestionStatusFullName($question->getStatus())); - $view->addProperty( pht('Author'), $viewer->renderHandle($question->getAuthorPHID())); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($question->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($question->getVoteCount()) ->setVote($question->getUserVote()); $view->addSectionHeader(pht('Question')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer)), )); return $view; } private function buildQuestionTransactions(PonderQuestion $question) { $viewer = $this->getViewer(); $id = $question->getID(); $timeline = $this->buildTransactionTimeline( $question, id(new PonderQuestionTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $xactions = $timeline->getTransactions(); $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); return $this->wrapComments( count($xactions), array( $timeline, $add_comment, )); } /** * This is fairly non-standard; building N timelines at once (N = number of * answers) is tricky business. * * TODO - re-factor this to ajax in one answer panel at a time in a more * standard fashion. This is necessary to scale this application. */ private function buildAnswers(array $answers) { - $request = $this->getRequest(); - $viewer = $request->getUser(); + $viewer = $this->getViewer(); $out = array(); $xactions = id(new PonderAnswerTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(mpull($answers, 'getPHID')) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $xaction_groups = mgroup($xactions, 'getObjectPHID'); foreach ($answers as $answer) { $author_phid = $answer->getAuthorPHID(); $xactions = idx($xaction_groups, $answer->getPHID(), array()); $id = $answer->getID(); $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); $out[] = id(new PhabricatorAnchorView()) ->setAnchorName("A$id"); $header = id(new PHUIHeaderView()) ->setHeader($viewer->renderHandle($author_phid)); $actions = $this->buildAnswerActions($answer); $properties = $this->buildAnswerProperties($answer, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $out[] = $object_box; $details = array(); $details[] = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Answer Comment')) ->setAction($this->getApplicationURI("/answer/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); $details[] = $form; $out[] = $this->wrapComments( count($xactions), $details); } $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); return $out; } private function buildAnswerActions(PonderAnswer $answer) { + $viewer = $this->getViewer(); $request = $this->getRequest(); - $viewer = $request->getUser(); - $id = $answer->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $answer, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) - ->setUser($request->getUser()) + ->setUser($viewer) ->setObject($answer) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Answer')) ->setHref($this->getApplicationURI("/answer/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/answer/history/{$id}/"))); return $view; } private function buildAnswerProperties( PonderAnswer $answer, PhabricatorActionListView $actions) { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($answer) ->setActionList($actions); $view->addProperty( pht('Created'), phabricator_datetime($answer->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($answer->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($answer->getVoteCount()) ->setVote($answer->getUserVote()); $view->addSectionHeader(pht('Answer')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $answer, $answer->getMarkupField(), $viewer)), )); return $view; } private function wrapComments($n, $stuff) { if ($n == 0) { $text = pht('Add a Comment'); } else { $text = pht('Show %s Comments', new PhutilNumber($n)); } $show_id = celerity_generate_unique_node_id(); $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); - require_celerity_resource('ponder-comment-table-css'); + require_celerity_resource('ponder-view-css'); $show = phutil_tag( 'div', array( 'id' => $show_id, 'class' => 'ponder-show-comments', ), javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => array($hide_id), 'hideIDs' => array($show_id), ), ), $text)); $hide = phutil_tag( 'div', array( + 'class' => 'ponder-comments-view', 'id' => $hide_id, 'style' => 'display: none', ), $stuff); return array($show, $hide); } } diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php index 9dbe58cb0e..af984f4b4d 100644 --- a/src/applications/ponder/editor/PonderQuestionEditor.php +++ b/src/applications/ponder/editor/PonderQuestionEditor.php @@ -1,251 +1,255 @@ answer = $answer; return $this; } private function getAnswer() { return $this->answer; } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: return true; } } return false; } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: $new_value = $xaction->getNewValue(); $new = idx($new_value, '+', array()); foreach ($new as $new_answer) { $answer = idx($new_answer, 'answer'); if (!$answer) { continue; } $answer->save(); $this->setAnswer($answer); } break; } } } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; + $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; + $types[] = PhabricatorTransactions::TYPE_SPACE; + $types[] = PonderQuestionTransaction::TYPE_TITLE; $types[] = PonderQuestionTransaction::TYPE_CONTENT; $types[] = PonderQuestionTransaction::TYPE_ANSWERS; $types[] = PonderQuestionTransaction::TYPE_STATUS; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_TITLE: return $object->getTitle(); case PonderQuestionTransaction::TYPE_CONTENT: return $object->getContent(); case PonderQuestionTransaction::TYPE_ANSWERS: return mpull($object->getAnswers(), 'getPHID'); case PonderQuestionTransaction::TYPE_STATUS: return $object->getStatus(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_TITLE: case PonderQuestionTransaction::TYPE_CONTENT: case PonderQuestionTransaction::TYPE_STATUS: return $xaction->getNewValue(); case PonderQuestionTransaction::TYPE_ANSWERS: $raw_new_value = $xaction->getNewValue(); $new_value = array(); foreach ($raw_new_value as $key => $answers) { $phids = array(); foreach ($answers as $answer) { $obj = idx($answer, 'answer'); if (!$answer) { continue; } $phids[] = $obj->getPHID(); } $new_value[$key] = $phids; } $xaction->setNewValue($new_value); return $this->getPHIDTransactionNewValue($xaction); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_TITLE: $object->setTitle($xaction->getNewValue()); break; case PonderQuestionTransaction::TYPE_CONTENT: $object->setContent($xaction->getNewValue()); break; case PonderQuestionTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); break; case PonderQuestionTransaction::TYPE_ANSWERS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_diff_key($new, $old); $rem = array_diff_key($old, $new); $count = $object->getAnswerCount(); $count += count($add); $count -= count($rem); $object->setAnswerCount($count); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PonderQuestionTransaction::TYPE_TITLE: case PonderQuestionTransaction::TYPE_CONTENT: case PonderQuestionTransaction::TYPE_STATUS: return $v; } return parent::mergeTransactions($u, $v); } protected function supportsSearch() { return true; } protected function getFeedStoryType() { return 'PonderTransactionFeedStory'; } protected function getFeedStoryData( PhabricatorLiskDAO $object, array $xactions) { $data = parent::getFeedStoryData($object, $xactions); $answer = $this->getAnswer(); if ($answer) { $data['answerPHID'] = $answer->getPHID(); } return $data; } protected function shouldImplyCC( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PonderQuestionTransaction::TYPE_ANSWERS: return true; } return parent::shouldImplyCC($object, $xaction); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PonderQuestionReplyHandler()) ->setMailReceiver($object); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $header = pht('QUESTION DETAIL'); $uri = '/Q'.$object->getID(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); // If the user just asked the question, add the question text. if ($type == PonderQuestionTransaction::TYPE_CONTENT) { if ($old === null) { $body->addRawSection($new); } } // If the user gave an answer, add the answer text. Also update // the header and uri to be more answer-specific. if ($type == PonderQuestionTransaction::TYPE_ANSWERS) { $answer = $this->getAnswer(); $body->addRawSection($answer->getContent()); $header = pht('ANSWER DETAIL'); $uri = $answer->getURI(); } } $body->addLinkSection( $header, PhabricatorEnv::getProductionURI($uri)); return $body; } } diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php index 78bc42a7d3..74d8230327 100644 --- a/src/applications/ponder/query/PonderQuestionQuery.php +++ b/src/applications/ponder/query/PonderQuestionQuery.php @@ -1,177 +1,203 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withAnswererPHIDs(array $phids) { $this->answererPHIDs = $phids; return $this; } public function needAnswers($need_answers) { $this->needAnswers = $need_answers; return $this; } public function needViewerVotes($need_viewer_votes) { $this->needViewerVotes = $need_viewer_votes; return $this; } + public function needProjectPHIDs($need_projects) { + $this->needProjectPHIDs = $need_projects; + return $this; + } + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'q.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'q.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'q.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->status !== null) { switch ($this->status) { case self::STATUS_ANY: break; case self::STATUS_OPEN: $where[] = qsprintf( $conn, 'q.status = %d', PonderQuestionStatus::STATUS_OPEN); break; case self::STATUS_CLOSED: $where[] = qsprintf( $conn, 'q.status = %d', PonderQuestionStatus::STATUS_CLOSED); break; default: throw new Exception(pht("Unknown status query '%s'!", $this->status)); } } return $where; } public function newResultObject() { return new PonderQuestion(); } protected function loadPage() { return $this->loadStandardPage(new PonderQuestion()); } protected function willFilterPage(array $questions) { + + $phids = mpull($questions, 'getPHID'); + if ($this->needAnswers) { $aquery = id(new PonderAnswerQuery()) ->setViewer($this->getViewer()) ->setOrderVector(array('-id')) ->withQuestionIDs(mpull($questions, 'getID')); if ($this->needViewerVotes) { $aquery->needViewerVotes($this->needViewerVotes); } $answers = $aquery->execute(); $answers = mgroup($answers, 'getQuestionID'); foreach ($questions as $question) { $question_answers = idx($answers, $question->getID(), array()); $question->attachAnswers(mpull($question_answers, null, 'getPHID')); } } if ($this->needViewerVotes) { $viewer_phid = $this->getViewer()->getPHID(); $etype = PonderQuestionHasVotingUserEdgeType::EDGECONST; $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($questions, 'getPHID')) + ->withSourcePHIDs($phids) ->withDestinationPHIDs(array($viewer_phid)) ->withEdgeTypes(array($etype)) ->needEdgeData(true) ->execute(); foreach ($questions as $question) { $user_edge = idx( $edges[$question->getPHID()][$etype], $viewer_phid, array()); $question->attachUserVote($viewer_phid, idx($user_edge, 'data', 0)); } } + if ($this->needProjectPHIDs) { + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($phids) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + $edge_query->execute(); + + foreach ($questions as $question) { + $project_phids = $edge_query->getDestinationPHIDs( + array($question->getPHID())); + $question->attachProjectPHIDs($project_phids); + } + } + return $questions; } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->answererPHIDs) { $answer_table = new PonderAnswer(); $joins[] = qsprintf( $conn_r, 'JOIN %T a ON a.questionID = q.id AND a.authorPHID IN (%Ls)', $answer_table->getTableName(), $this->answererPHIDs); } return implode(' ', $joins); } protected function getPrimaryTableAlias() { return 'q'; } public function getQueryApplicationClass() { return 'PhabricatorPonderApplication'; } } diff --git a/src/applications/ponder/query/PonderQuestionSearchEngine.php b/src/applications/ponder/query/PonderQuestionSearchEngine.php index 885e39bbf9..1b475c7b58 100644 --- a/src/applications/ponder/query/PonderQuestionSearchEngine.php +++ b/src/applications/ponder/query/PonderQuestionSearchEngine.php @@ -1,152 +1,177 @@ needProjectPHIDs(true); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } if ($map['answerers']) { $query->withAnswererPHIDs($map['answerers']); } $status = $map['status']; if ($status != null) { switch ($status) { case 0: $query->withStatus(PonderQuestionQuery::STATUS_OPEN); break; case 1: $query->withStatus(PonderQuestionQuery::STATUS_CLOSED); break; } } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorUsersSearchField()) ->setKey('authorPHIDs') ->setAliases(array('authors')) ->setLabel(pht('Authors')), id(new PhabricatorUsersSearchField()) ->setKey('answerers') ->setAliases(array('answerers')) ->setLabel(pht('Answered By')), id(new PhabricatorSearchSelectField()) ->setLabel(pht('Status')) ->setKey('status') ->setOptions(PonderQuestionStatus::getQuestionStatusMap()), ); } protected function getURI($path) { return '/ponder/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'open' => pht('Open Questions'), 'all' => pht('All Questions'), ); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); $names['answered'] = pht('Answered'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'open': return $query->setParameter('status', PonderQuestionQuery::STATUS_OPEN); case 'authored': return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); case 'answered': return $query->setParameter( 'answererPHIDs', array($this->requireViewer()->getPHID())); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $questions, PhabricatorSavedQuery $query) { return mpull($questions, 'getAuthorPHID'); } protected function renderResultList( array $questions, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($questions, 'PonderQuestion'); $viewer = $this->requireViewer(); + $proj_phids = array(); + foreach ($questions as $question) { + foreach ($question->getProjectPHIDs() as $project_phid) { + $proj_phids[] = $project_phid; + } + } + + $proj_handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($proj_phids) + ->execute(); + $view = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($questions as $question) { $color = PonderQuestionStatus::getQuestionStatusTagColor( $question->getStatus()); $icon = PonderQuestionStatus::getQuestionStatusIcon( $question->getStatus()); $full_status = PonderQuestionStatus::getQuestionStatusFullName( $question->getStatus()); $item = new PHUIObjectItemView(); $item->setObjectName('Q'.$question->getID()); $item->setHeader($question->getTitle()); $item->setHref('/Q'.$question->getID()); $item->setObject($question); $item->setStatusIcon($icon.' '.$color, $full_status); + $project_handles = array_select_keys( + $proj_handles, + $question->getProjectPHIDs()); + $created_date = phabricator_date($question->getDateCreated(), $viewer); $item->addIcon('none', $created_date); $item->addByline( pht( 'Asked by %s', $handles[$question->getAuthorPHID()]->renderLink())); $item->addAttribute( pht('%d Answer(s)', $question->getAnswerCount())); + if ($project_handles) { + $item->addAttribute( + id(new PHUIHandleTagListView()) + ->setLimit(4) + ->setSlim(true) + ->setHandles($project_handles)); + } + $view->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($view); $result->setNoDataString(pht('No questions found.')); return $result; } } diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 57e0e2ea21..10ada70c5e 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -1,261 +1,270 @@ question = $question; return $this; } public function getQuestion() { return $this->assertAttached($this->question); } public function getURI() { return '/Q'.$this->getQuestionID().'#A'.$this->getID(); } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderVote::VOTE_NONE; } return $this; } public function attachUserVote($user_phid, $vote) { $this->vote = $vote; return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'voteCount' => 'sint32', 'content' => 'text', + 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'key_oneanswerperquestion' => array( 'columns' => array('questionID', 'authorPHID'), 'unique' => true, ), 'questionID' => array( 'columns' => array('questionID'), ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderAnswerPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderAnswerEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PonderAnswerTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:A{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PonderVotingUserHasAnswerEdgeType::EDGECONST; } public function getVotablePHID() { return $this->getPHID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getQuestion()->getPolicy($capability); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->getAuthorPHID() == $viewer->getPHID()) { return true; } return $this->getQuestion()->hasAutomaticCapability( $capability, $viewer); case PhabricatorPolicyCapability::CAN_EDIT: return ($this->getAuthorPHID() == $viewer->getPHID()); } } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The author of an answer can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'The user who asks a question can always view the answers.'); break; } return $out; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index 1568c59245..f2d81c4838 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,301 +1,343 @@ setViewer($actor) + ->withClasses(array('PhabricatorPonderApplication')) + ->executeOne(); + + $view_policy = $app->getPolicy( + PonderQuestionDefaultViewCapability::CAPABILITY); + $edit_policy = $app->getPolicy( + PonderQuestionDefaultEditCapability::CAPABILITY); + + return id(new PonderQuestion()) + ->setAuthorPHID($actor->getPHID()) + ->setViewPolicy($view_policy) + ->setEditPolicy($edit_policy) + ->setStatus(PonderQuestionStatus::STATUS_OPEN) + ->setVoteCount(0) + ->setAnswerCount(0) + ->setHeat(0.0) + ->setSpacePHID($actor->getDefaultSpacePHID()); + } + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'voteCount' => 'sint32', 'status' => 'uint32', 'content' => 'text', 'heat' => 'double', 'answerCount' => 'uint32', 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'heat' => array( 'columns' => array('heat'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function attachVotes($user_phid) { $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($user_phid)) ->withDestinationPHIDs($qa_phids) ->withEdgeTypes( array( PonderVotingUserHasQuestionEdgeType::EDGECONST, PonderVotingUserHasAnswerEdgeType::EDGECONST, )) ->needEdgeData(true) ->execute(); $question_edge = $edges[$user_phid][PonderVotingUserHasQuestionEdgeType::EDGECONST]; $answer_edges = $edges[$user_phid][PonderVotingUserHasAnswerEdgeType::EDGECONST]; $edges = null; $this->setUserVote(idx($question_edge, $this->getPHID())); foreach ($this->answers as $answer) { $answer->setUserVote(idx($answer_edges, $answer->getPHID())); } } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderVote::VOTE_NONE; } return $this; } public function attachUserVote($user_phid, $vote) { $this->vote = $vote; return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function attachAnswers(array $answers) { assert_instances_of($answers, 'PonderAnswer'); $this->answers = $answers; return $this; } public function getAnswers() { return $this->answers; } + public function getProjectPHIDs() { + return $this->assertAttached($this->projectPHIDs); + } + + public function attachProjectPHIDs(array $phids) { + $this->projectPHIDs = $phids; + return $this; + } + public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PonderQuestionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PonderQuestionTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:Q{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PonderVotingUserHasQuestionEdgeType::EDGECONST; } public function getVotablePHID() { return $this->getPHID(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getOriginalTitle() { // TODO: Make this actually save/return the original title. return $this->getTitle(); } public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); return "Q{$id}: {$title}"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - $policy = PhabricatorPolicies::POLICY_NOONE; - switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: - $policy = PhabricatorPolicies::POLICY_USER; - break; + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); } - - return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The user who asked a question can always view and edit it.'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $answers = id(new PonderAnswer())->loadAllWhere( 'questionID = %d', $this->getID()); foreach ($answers as $answer) { $engine->destroyObject($answer); } $this->delete(); $this->saveTransaction(); } + +/* -( PhabricatorSpacesInterface )----------------------------------------- */ + + + public function getSpacePHID() { + return $this->spacePHID; + } + } diff --git a/src/applications/ponder/view/PonderVotableView.php b/src/applications/ponder/view/PonderVotableView.php index 11cbe6cde0..dbd6ba32a8 100644 --- a/src/applications/ponder/view/PonderVotableView.php +++ b/src/applications/ponder/view/PonderVotableView.php @@ -1,92 +1,92 @@ phid = $phid; return $this; } public function setURI($uri) { $this->uri = $uri; return $this; } public function setCount($count) { $this->count = $count; return $this; } public function setVote($vote) { $this->vote = $vote; return $this; } public function render() { - require_celerity_resource('ponder-vote-css'); + require_celerity_resource('ponder-view-css'); require_celerity_resource('javelin-behavior-ponder-votebox'); Javelin::initBehavior('ponder-votebox', array()); $uri = id(new PhutilURI($this->uri))->alter('phid', $this->phid); $up = javelin_tag( 'a', array( 'href' => (string)$uri, 'sigil' => 'upvote', 'mustcapture' => true, 'class' => ($this->vote > 0) ? 'ponder-vote-active' : null, ), "\xE2\x96\xB2"); $down = javelin_tag( 'a', array( 'href' => (string)$uri, 'sigil' => 'downvote', 'mustcapture' => true, 'class' => ($this->vote < 0) ? 'ponder-vote-active' : null, ), "\xE2\x96\xBC"); $count = javelin_tag( 'div', array( 'class' => 'ponder-vote-count', 'sigil' => 'ponder-vote-count', ), $this->count); return javelin_tag( 'div', array( 'class' => 'ponder-votable', 'sigil' => 'ponder-votable', 'meta' => array( 'count' => (int)$this->count, 'vote' => (int)$this->vote, ), ), array( javelin_tag( 'div', array( 'class' => 'ponder-votebox', ), array($up, $count, $down)), phutil_tag( 'div', array( 'class' => 'ponder-votebox-content', ), $this->renderChildren()), )); } } diff --git a/src/applications/project/herald/PhabricatorProjectAddHeraldAction.php b/src/applications/project/herald/PhabricatorProjectAddHeraldAction.php new file mode 100644 index 0000000000..5d2a348eca --- /dev/null +++ b/src/applications/project/herald/PhabricatorProjectAddHeraldAction.php @@ -0,0 +1,28 @@ +applyProjects($effect->getTarget(), $is_add = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorProjectDatasource(); + } + + public function renderActionDescription($value) { + return pht('Add projects: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/project/herald/PhabricatorProjectHeraldAction.php b/src/applications/project/herald/PhabricatorProjectHeraldAction.php new file mode 100644 index 0000000000..a720ebe5b0 --- /dev/null +++ b/src/applications/project/herald/PhabricatorProjectHeraldAction.php @@ -0,0 +1,125 @@ +getAdapter(); + + $allowed_types = array( + PhabricatorProjectProjectPHIDType::TYPECONST, + ); + + // Detection of "No Effect" is a bit tricky for this action, so just do it + // manually a little later on. + $current = array(); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + $current = $adapter->loadEdgePHIDs($project_type); + + if ($is_add) { + $already = array(); + foreach ($phids as $phid) { + if (isset($current[$phid])) { + $already[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($already) { + $this->logEffect(self::DO_STANDARD_NO_EFFECT, $already); + } + } else { + $already = array(); + foreach ($phids as $phid) { + if (empty($current[$phid])) { + $already[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($already) { + $this->logEffect(self::DO_STANDARD_NO_EFFECT, $already); + } + } + + if (!$phids) { + return; + } + + if ($is_add) { + $kind = '+'; + } else { + $kind = '-'; + } + + $xaction = $adapter->newTransaction() + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $project_type) + ->setNewValue( + array( + $kind => $phids, + )); + + $adapter->queueTransaction($xaction); + + if ($is_add) { + $this->logEffect(self::DO_ADD_PROJECTS, $phids); + } else { + $this->logEffect(self::DO_REMOVE_PROJECTS, $phids); + } + } + + protected function getActionEffectMap() { + return array( + self::DO_ADD_PROJECTS => array( + 'icon' => 'fa-briefcase', + 'color' => 'green', + 'name' => pht('Added Projects'), + ), + self::DO_REMOVE_PROJECTS => array( + 'icon' => 'fa-minus-circle', + 'color' => 'green', + 'name' => pht('Removed Projects'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_ADD_PROJECTS: + return pht( + 'Added %s project(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_REMOVE_PROJECTS: + return pht( + 'Removed %s project(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + } + } + +} diff --git a/src/applications/project/herald/PhabricatorProjectRemoveHeraldAction.php b/src/applications/project/herald/PhabricatorProjectRemoveHeraldAction.php new file mode 100644 index 0000000000..77e7071b13 --- /dev/null +++ b/src/applications/project/herald/PhabricatorProjectRemoveHeraldAction.php @@ -0,0 +1,28 @@ +applyProjects($effect->getTarget(), $is_add = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorProjectDatasource(); + } + + public function renderActionDescription($value) { + return pht('Remove projects: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/releeph/controller/branch/ReleephBranchAccessController.php b/src/applications/releeph/controller/branch/ReleephBranchAccessController.php index 177fb82c8f..8b675ed2da 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchAccessController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchAccessController.php @@ -1,81 +1,73 @@ action = $data['action']; - $this->branchID = $data['branchID']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $action = $request->getURIData('action'); + $id = $request->getURIData('branchID'); $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) - ->withIDs(array($this->branchID)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $this->setBranch($branch); - $action = $this->action; switch ($action) { case 'close': case 're-open': break; default: return new Aphront404Response(); } $branch_uri = $this->getBranchViewURI($branch); if ($request->isFormPost()) { if ($action == 're-open') { $is_active = 1; } else { $is_active = 0; } id(new ReleephBranchEditor()) ->setActor($request->getUser()) ->setReleephBranch($branch) ->changeBranchAccess($is_active); return id(new AphrontReloadResponse())->setURI($branch_uri); } if ($action == 'close') { $title_text = pht('Really Close Branch?'); $short = pht('Close Branch'); $body_text = pht( 'Really close the branch "%s"?', phutil_tag('strong', array(), $branch->getBasename())); $button_text = pht('Close Branch'); } else { $title_text = pht('Really Reopen Branch?'); $short = pht('Reopen Branch'); $body_text = pht( 'Really reopen the branch "%s"?', phutil_tag('strong', array(), $branch->getBasename())); $button_text = pht('Reopen Branch'); } return $this->newDialog() ->setTitle($title_text) ->setShortTitle($short) ->appendChild($body_text) ->addSubmitButton($button_text) ->addCancelButton($branch_uri); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php index 851724c6d9..d13383cc3b 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php @@ -1,130 +1,124 @@ productID = $data['projectID']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('projectID'); $product = id(new ReleephProductQuery()) ->setViewer($viewer) - ->withIDs(array($this->productID)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$product) { return new Aphront404Response(); } $this->setProduct($product); $cut_point = $request->getStr('cutPoint'); $symbolic_name = $request->getStr('symbolicName'); if (!$cut_point) { $repository = $product->getRepository(); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $cut_point = $product->getTrunkBranch(); break; } } $e_cut = true; $errors = array(); $branch_date_control = id(new AphrontFormDateControl()) ->setUser($request->getUser()) ->setName('templateDate') ->setLabel(pht('Date')) ->setCaption(pht('The date used for filling out the branch template.')) ->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY); $branch_date = $branch_date_control->readValueFromRequest($request); if ($request->isFormPost()) { $cut_commit = null; if (!$cut_point) { $e_cut = pht('Required'); $errors[] = pht('You must give a branch cut point'); } else { try { $finder = id(new ReleephCommitFinder()) ->setUser($request->getUser()) ->setReleephProject($product); $cut_commit = $finder->fromPartial($cut_point); } catch (Exception $e) { $e_cut = pht('Invalid'); $errors[] = $e->getMessage(); } } if (!$errors) { $branch = id(new ReleephBranchEditor()) ->setReleephProject($product) ->setActor($request->getUser()) ->newBranchFromCommit( $cut_commit, $branch_date, $symbolic_name); $branch_uri = $this->getApplicationURI('branch/'.$branch->getID()); return id(new AphrontRedirectResponse()) ->setURI($branch_uri); } } $product_uri = $this->getProductViewURI($product); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Symbolic Name')) ->setName('symbolicName') ->setValue($symbolic_name) ->setCaption(pht( 'Mutable alternate name, for easy reference, (e.g. "LATEST")'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Cut point')) ->setName('cutPoint') ->setValue($cut_point) ->setError($e_cut) ->setCaption(pht( 'A commit ID for your repo type, or a Diffusion ID like "rE123"'))) ->appendChild($branch_date_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Cut Branch')) ->addCancelButton($product_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('New Branch')) ->setFormErrors($errors) ->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('New Branch')); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => pht('New Branch'), )); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchEditController.php b/src/applications/releeph/controller/branch/ReleephBranchEditController.php index 000e9535bf..6d66f5d9d5 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchEditController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchEditController.php @@ -1,115 +1,108 @@ branchID = $data['branchID']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('branchID'); $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) - ->withIDs(array($this->branchID)) + ->withIDs(array($id)) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $this->setBranch($branch); $symbolic_name = $request->getStr( 'symbolicName', $branch->getSymbolicName()); if ($request->isFormPost()) { $existing_with_same_symbolic_name = id(new ReleephBranch()) ->loadOneWhere( 'id != %d AND releephProjectID = %d AND symbolicName = %s', $branch->getID(), $branch->getReleephProjectID(), $symbolic_name); $branch->openTransaction(); - $branch - ->setSymbolicName($symbolic_name); + $branch->setSymbolicName($symbolic_name); if ($existing_with_same_symbolic_name) { $existing_with_same_symbolic_name ->setSymbolicName(null) ->save(); } $branch->save(); $branch->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI($this->getBranchViewURI($branch)); } $phids = array(); $phids[] = $creator_phid = $branch->getCreatedByUserPHID(); $phids[] = $cut_commit_phid = $branch->getCutPointCommitPHID(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($request->getUser()) ->withPHIDs($phids) ->execute(); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Branch Name')) ->setValue($branch->getName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Cut Point')) ->setValue($handles[$cut_commit_phid]->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Created By')) ->setValue($handles[$creator_phid]->renderLink())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Symbolic Name')) ->setName('symbolicName') ->setValue($symbolic_name) ->setCaption(pht( 'Mutable alternate name, for easy reference, (e.g. "LATEST")'))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getBranchViewURI($branch)) ->setValue(pht('Save Branch'))); $title = pht( 'Edit Branch %s', $branch->getDisplayNameWithDetail()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Edit')); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php b/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php index 46c0b47f6c..a77cdf8fb3 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php @@ -1,47 +1,41 @@ branchID = $data['branchID']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('branchID'); $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) - ->withIDs(array($this->branchID)) + ->withIDs(array($id)) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $this->setBranch($branch); $timeline = $this->buildTransactionTimeline( $branch, new ReleephBranchTransactionQuery()); $timeline ->setShouldTerminate(true); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Branch History'), )); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchNamePreviewController.php b/src/applications/releeph/controller/branch/ReleephBranchNamePreviewController.php index bf429f9034..f2dbd23d8d 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchNamePreviewController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchNamePreviewController.php @@ -1,48 +1,47 @@ getRequest(); + public function handleRequest(AphrontRequest $request) { $is_symbolic = $request->getBool('isSymbolic'); $template = $request->getStr('template'); if (!$is_symbolic && !$template) { $template = ReleephBranchTemplate::getDefaultTemplate(); } $repository_phid = $request->getInt('repositoryPHID'); $fake_commit_handle = ReleephBranchTemplate::getFakeCommitHandleFor( $repository_phid, $request->getUser()); list($name, $errors) = id(new ReleephBranchTemplate()) ->setCommitHandle($fake_commit_handle) ->setReleephProjectName($request->getStr('projectName')) ->setSymbolic($is_symbolic) ->interpolate($template); $markup = ''; if ($name) { $markup = phutil_tag( 'div', array('class' => 'name'), $name); } if ($errors) { $markup .= phutil_tag( 'div', array('class' => 'error'), head($errors)); } return id(new AphrontAjaxResponse()) ->setContent(array('markup' => $markup)); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchViewController.php b/src/applications/releeph/controller/branch/ReleephBranchViewController.php index 29e0f6d658..11615fe3ed 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchViewController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchViewController.php @@ -1,157 +1,150 @@ branchID = $data['branchID']; - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('branchID'); + $querykey = $request->getURIData('queryKey'); $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) - ->withIDs(array($this->branchID)) + ->withIDs(array($id)) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $this->setBranch($branch); $controller = id(new PhabricatorApplicationSearchController()) ->setPreface($this->renderPreface()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine($this->getSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView($for_app = false) { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $this->getSearchEngine()->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } private function getSearchEngine() { $branch = $this->getBranch(); return id(new ReleephRequestSearchEngine()) ->setBranch($branch) ->setBaseURI($this->getApplicationURI('branch/'.$branch->getID().'/')) ->setViewer($this->getRequest()->getUser()); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $branch = $this->getBranch(); if ($branch) { $pull_uri = $this->getApplicationURI('branch/pull/'.$branch->getID().'/'); $crumbs->addAction( id(new PHUIListItemView()) ->setHref($pull_uri) ->setName(pht('New Pull Request')) ->setIcon('fa-plus-square') ->setDisabled(!$branch->isActive())); } return $crumbs; } private function renderPreface() { $viewer = $this->getRequest()->getUser(); $branch = $this->getBranch(); $id = $branch->getID(); $header = id(new PHUIHeaderView()) ->setHeader($branch->getDisplayName()) ->setUser($viewer) ->setPolicyObject($branch); if ($branch->getIsActive()) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { $header->setStatus('fa-ban', 'dark', pht('Closed')); } $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($branch) ->setObjectURI($this->getRequest()->getRequestURI()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $branch, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = $this->getApplicationURI("branch/edit/{$id}/"); $close_uri = $this->getApplicationURI("branch/close/{$id}/"); $reopen_uri = $this->getApplicationURI("branch/re-open/{$id}/"); $history_uri = $this->getApplicationURI("branch/{$id}/history/"); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Branch')) ->setHref($edit_uri) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($branch->getIsActive()) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Close Branch')) ->setHref($close_uri) ->setIcon('fa-times') ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Reopen Branch')) ->setHref($reopen_uri) ->setIcon('fa-plus') ->setUser($viewer) ->setDisabled(!$can_edit) ->setWorkflow(true)); } $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-list')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($branch) ->setActionList($actions); $properties->addProperty( pht('Branch'), $branch->getName()); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } } diff --git a/src/applications/settings/controller/PhabricatorSettingsAdjustController.php b/src/applications/settings/controller/PhabricatorSettingsAdjustController.php index 2f6b2bcab1..7211431f16 100644 --- a/src/applications/settings/controller/PhabricatorSettingsAdjustController.php +++ b/src/applications/settings/controller/PhabricatorSettingsAdjustController.php @@ -1,18 +1,17 @@ getRequest(); + public function handleRequest(AphrontRequest $request) { $user = $request->getUser(); $prefs = $user->loadPreferences(); $prefs->setPreference( $request->getStr('key'), $request->getStr('value')); $prefs->save(); return id(new AphrontAjaxResponse())->setContent(array()); } } diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php index 80753b17ca..7a2799935c 100644 --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -1,161 +1,155 @@ user; } private function isSelf() { - $viewer_phid = $this->getRequest()->getUser()->getPHID(); + $viewer_phid = $this->getViewer()->getPHID(); $user_phid = $this->getUser()->getPHID(); return ($viewer_phid == $user_phid); } - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - $this->key = idx($data, 'key'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + $key = $request->getURIData('key'); - if ($this->id) { + if ($id) { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$user) { return new Aphront404Response(); } $this->user = $user; } else { $this->user = $viewer; } $panels = $this->buildPanels(); $nav = $this->renderSideNav($panels); - $key = $nav->selectFilter($this->key, head($panels)->getPanelKey()); + $key = $nav->selectFilter($key, head($panels)->getPanelKey()); $panel = $panels[$key]; $panel->setUser($this->getUser()); $panel->setViewer($viewer); $response = $panel->processRequest($request); if ($response instanceof AphrontResponse) { return $response; } $crumbs = $this->buildApplicationCrumbs(); if (!$this->isSelf()) { $crumbs->addTextCrumb( $this->getUser()->getUsername(), '/p/'.$this->getUser()->getUsername().'/'); } $crumbs->addTextCrumb($panel->getPanelName()); $nav->appendChild( array( $crumbs, $response, )); return $this->buildApplicationPage( $nav, array( 'title' => $panel->getPanelName(), )); } private function buildPanels() { $panel_specs = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorSettingsPanel') ->setConcreteOnly(true) ->selectAndLoadSymbols(); $panels = array(); foreach ($panel_specs as $spec) { $class = newv($spec['name'], array()); $panels[] = $class->buildPanels(); } $panels = array_mergev($panels); $panels = mpull($panels, null, 'getPanelKey'); $result = array(); foreach ($panels as $key => $panel) { $panel->setUser($this->user); if (!$panel->isEnabled()) { continue; } if (!$this->isSelf()) { if (!$panel->isEditableByAdministrators()) { continue; } } if (!empty($result[$key])) { throw new Exception(pht( "Two settings panels share the same panel key ('%s'): %s, %s.", $key, get_class($panel), get_class($result[$key]))); } $result[$key] = $panel; } $result = msort($result, 'getPanelSortKey'); if (!$result) { throw new Exception(pht('No settings panels are available.')); } return $result; } private function renderSideNav(array $panels) { $nav = new AphrontSideNavFilterView(); if ($this->isSelf()) { $base_uri = 'panel/'; } else { $base_uri = $this->getUser()->getID().'/panel/'; } $nav->setBaseURI(new PhutilURI($this->getApplicationURI($base_uri))); $group = null; foreach ($panels as $panel) { if ($panel->getPanelGroup() != $group) { $group = $panel->getPanelGroup(); $nav->addLabel($group); } $nav->addFilter($panel->getPanelKey(), $panel->getPanelName()); } return $nav; } public function buildApplicationMenu() { $panels = $this->buildPanels(); return $this->renderSideNav($panels)->getMenu(); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteCloseController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteCloseController.php index c4e4fed588..05c12aec27 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteCloseController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteCloseController.php @@ -1,71 +1,65 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $poll = id(new PhabricatorSlowvoteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $close_uri = '/V'.$poll->getID(); if ($request->isFormPost()) { if ($poll->getIsClosed()) { $new_status = 0; } else { $new_status = 1; } $xactions = array(); $xactions[] = id(new PhabricatorSlowvoteTransaction()) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_CLOSE) ->setNewValue($new_status); id(new PhabricatorSlowvoteEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($poll, $xactions); return id(new AphrontRedirectResponse())->setURI($close_uri); } if ($poll->getIsClosed()) { $title = pht('Reopen Poll'); $content = pht('Are you sure you want to reopen the poll?'); $submit = pht('Reopen'); } else { $title = pht('Close Poll'); $content = pht('Are you sure you want to close the poll?'); $submit = pht('Close'); } return $this->newDialog() ->setTitle($title) ->appendChild($content) ->addSubmitButton($submit) ->addCancelButton($close_uri); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php index 588f06b649..035eb577d8 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php @@ -1,69 +1,63 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $poll = id(new PhabricatorSlowvoteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); $view_uri = '/V'.$poll->getID(); $xactions = array(); $xactions[] = id(new PhabricatorSlowvoteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorSlowvoteTransactionComment()) ->setContent($request->getStr('comment'))); $editor = id(new PhabricatorSlowvoteEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($poll, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($user) + ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php index 44d9cec792..e497a50fbc 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php @@ -1,285 +1,278 @@ getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->id) { + if ($id) { $poll = id(new PhabricatorSlowvoteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $is_new = false; } else { - $poll = PhabricatorSlowvotePoll::initializeNewPoll($user); + $poll = PhabricatorSlowvotePoll::initializeNewPoll($viewer); $is_new = true; } if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $poll->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $e_question = true; $e_response = true; $errors = array(); $v_question = $poll->getQuestion(); $v_description = $poll->getDescription(); $v_responses = $poll->getResponseVisibility(); $v_shuffle = $poll->getShuffle(); $v_space = $poll->getSpacePHID(); $responses = $request->getArr('response'); if ($request->isFormPost()) { $v_question = $request->getStr('question'); $v_description = $request->getStr('description'); $v_responses = (int)$request->getInt('responses'); $v_shuffle = (int)$request->getBool('shuffle'); $v_view_policy = $request->getStr('viewPolicy'); $v_projects = $request->getArr('projects'); $v_space = $request->getStr('spacePHID'); if ($is_new) { $poll->setMethod($request->getInt('method')); } if (!strlen($v_question)) { $e_question = pht('Required'); $errors[] = pht('You must ask a poll question.'); } else { $e_question = null; } if ($is_new) { $responses = array_filter($responses); if (empty($responses)) { $errors[] = pht('You must offer at least one response.'); $e_response = pht('Required'); } else { $e_response = null; } } $xactions = array(); $template = id(new PhabricatorSlowvoteTransaction()); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_QUESTION) ->setNewValue($v_question); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION) ->setNewValue($v_description); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_RESPONSES) ->setNewValue($v_responses); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_SHUFFLE) ->setNewValue($v_shuffle); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view_policy); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) ->setNewValue($v_space); if (empty($errors)) { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhabricatorSlowvoteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PhabricatorSlowvoteEditor()) - ->setActor($user) + ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($poll, $xactions); if ($is_new) { $poll->save(); foreach ($responses as $response) { $option = new PhabricatorSlowvoteOption(); $option->setName($response); $option->setPollID($poll->getID()); $option->save(); } } return id(new AphrontRedirectResponse()) ->setURI('/V'.$poll->getID()); } else { $poll->setViewPolicy($v_view_policy); } } $instructions = phutil_tag( 'p', array( 'class' => 'aphront-form-instructions', ), pht('Resolve issues and build consensus through '. 'protracted deliberation.')); $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendChild($instructions) ->appendChild( id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setLabel(pht('Question')) ->setName('question') ->setValue($v_question) ->setError($e_question)) ->appendChild( id(new PhabricatorRemarkupControl()) - ->setUser($user) + ->setUser($viewer) ->setLabel(pht('Description')) ->setName('description') ->setValue($v_description)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())); if ($is_new) { for ($ii = 0; $ii < 10; $ii++) { $n = ($ii + 1); $response = id(new AphrontFormTextControl()) ->setLabel(pht('Response %d', $n)) ->setName('response[]') ->setValue(idx($responses, $ii, '')); if ($ii == 0) { $response->setError($e_response); } $form->appendChild($response); } } $poll_type_options = array( PhabricatorSlowvotePoll::METHOD_PLURALITY => pht('Plurality (Single Choice)'), PhabricatorSlowvotePoll::METHOD_APPROVAL => pht('Approval (Multiple Choice)'), ); $response_type_options = array( PhabricatorSlowvotePoll::RESPONSES_VISIBLE => pht('Allow anyone to see the responses'), PhabricatorSlowvotePoll::RESPONSES_VOTERS => pht('Require a vote to see the responses'), PhabricatorSlowvotePoll::RESPONSES_OWNER => pht('Only I can see the responses'), ); if ($is_new) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Vote Type')) ->setName('method') ->setValue($poll->getMethod()) ->setOptions($poll_type_options)); } else { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Vote Type')) ->setValue(idx($poll_type_options, $poll->getMethod()))); } if ($is_new) { $title = pht('Create Slowvote'); $button = pht('Create'); $cancel_uri = $this->getApplicationURI(); } else { $title = pht('Edit %s', 'V'.$poll->getID()); $button = pht('Save Changes'); $cancel_uri = '/V'.$poll->getID(); } $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($poll) ->execute(); $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Responses')) ->setName('responses') ->setValue($v_responses) ->setOptions($response_type_options)) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Shuffle')) ->addCheckbox( 'shuffle', 1, pht('Show choices in random order.'), $v_shuffle)) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setName('viewPolicy') ->setPolicyObject($poll) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setSpacePHID($v_space)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb($title); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php index 8062fc4098..bbb96d4dc4 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php @@ -1,25 +1,21 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $querykey = $request->getURIData('queryKey'); - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($querykey) ->setSearchEngine(new PhabricatorSlowvoteSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php index 9bac0a6088..40980e5f41 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php @@ -1,160 +1,154 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $poll = id(new PhabricatorSlowvoteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needOptions(true) ->needChoices(true) ->needViewerChoices(true) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $poll_view = id(new SlowvoteEmbedView()) ->setHeadless(true) - ->setUser($user) + ->setUser($viewer) ->setPoll($poll); if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'pollID' => $poll->getID(), 'contentHTML' => $poll_view->render(), )); } $header_icon = $poll->getIsClosed() ? 'fa-ban' : 'fa-circle-o'; $header_name = $poll->getIsClosed() ? pht('Closed') : pht('Open'); $header_color = $poll->getIsClosed() ? 'dark' : 'bluegrey'; $header = id(new PHUIHeaderView()) ->setHeader($poll->getQuestion()) - ->setUser($user) + ->setUser($viewer) ->setStatus($header_icon, $header_color, $header_name) ->setPolicyObject($poll); $actions = $this->buildActionView($poll); $properties = $this->buildPropertyView($poll, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('V'.$poll->getID()); $timeline = $this->buildTransactionTimeline( $poll, new PhabricatorSlowvoteTransactionQuery()); $add_comment = $this->buildCommentForm($poll); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $poll_view, $timeline, $add_comment, ), array( 'title' => 'V'.$poll->getID().' '.$poll->getQuestion(), 'pageObjects' => array($poll->getPHID()), )); } private function buildActionView(PhabricatorSlowvotePoll $poll) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($poll); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $poll, PhabricatorPolicyCapability::CAN_EDIT); $is_closed = $poll->getIsClosed(); $close_poll_text = $is_closed ? pht('Reopen Poll') : pht('Close Poll'); $close_poll_icon = $is_closed ? 'fa-play-circle-o' : 'fa-ban'; $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Poll')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('edit/'.$poll->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName($close_poll_text) ->setIcon($close_poll_icon) ->setHref($this->getApplicationURI('close/'.$poll->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( PhabricatorSlowvotePoll $poll, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($poll) ->setActionList($actions); $view->invokeWillRenderEvent(); if (strlen($poll->getDescription())) { $view->addTextContent( $output = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent( $poll->getDescription()), 'default', $viewer)); } return $view; } private function buildCommentForm(PhabricatorSlowvotePoll $poll) { $viewer = $this->getRequest()->getUser(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Enter Deliberations'); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $poll->getPHID()); return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($poll->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$poll->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php index 6cfeae0ac0..2d630bffc1 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php @@ -1,109 +1,103 @@ id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $id = $request->getURIData('id'); $poll = id(new PhabricatorSlowvoteQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) + ->setViewer($viewer) + ->withIDs(array($id)) ->needOptions(true) ->needViewerChoices(true) ->executeOne(); if (!$poll) { return new Aphront404Response(); } if ($poll->getIsClosed()) { return new Aphront400Response(); } $options = $poll->getOptions(); - $user_choices = $poll->getViewerChoices($user); + $viewer_choices = $poll->getViewerChoices($viewer); - $old_votes = mpull($user_choices, null, 'getOptionID'); + $old_votes = mpull($viewer_choices, null, 'getOptionID'); if ($request->isAjax()) { $vote = $request->getInt('vote'); $votes = array_keys($old_votes); $votes = array_fuse($votes, $votes); if ($poll->getMethod() == PhabricatorSlowvotePoll::METHOD_PLURALITY) { if (idx($votes, $vote, false)) { $votes = array(); } else { $votes = array($vote); } } else { if (idx($votes, $vote, false)) { unset($votes[$vote]); } else { $votes[$vote] = $vote; } } - $this->updateVotes($user, $poll, $old_votes, $votes); + $this->updateVotes($viewer, $poll, $old_votes, $votes); $updated_choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID = %d AND authorPHID = %s', $poll->getID(), - $user->getPHID()); + $viewer->getPHID()); $embed = id(new SlowvoteEmbedView()) ->setPoll($poll) ->setOptions($options) ->setViewerChoices($updated_choices); return id(new AphrontAjaxResponse()) ->setContent(array( 'pollID' => $poll->getID(), 'contentHTML' => $embed->render(), )); } if (!$request->isFormPost()) { return id(new Aphront404Response()); } $votes = $request->getArr('vote'); $votes = array_fuse($votes, $votes); - $this->updateVotes($user, $poll, $old_votes, $votes); + $this->updateVotes($viewer, $poll, $old_votes, $votes); return id(new AphrontRedirectResponse())->setURI('/V'.$poll->getID()); } - private function updateVotes($user, $poll, $old_votes, $votes) { + private function updateVotes($viewer, $poll, $old_votes, $votes) { if (!empty($votes) && count($votes) > 1 && $poll->getMethod() == PhabricatorSlowvotePoll::METHOD_PLURALITY) { return id(new Aphront400Response()); } foreach ($old_votes as $old_vote) { if (!idx($votes, $old_vote->getOptionID(), false)) { $old_vote->delete(); } } foreach ($votes as $vote) { if (idx($old_votes, $vote, false)) { continue; } id(new PhabricatorSlowvoteChoice()) - ->setAuthorPHID($user->getPHID()) + ->setAuthorPHID($viewer->getPHID()) ->setPollID($poll->getID()) ->setOptionID($vote) ->save(); } } } diff --git a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php index d1bd6c18f0..047a5c99a8 100644 --- a/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php +++ b/src/applications/slowvote/editor/PhabricatorSlowvoteEditor.php @@ -1,114 +1,181 @@ getOldValue(); $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: if ($old === null) { return true; } return ((int)$old !== (int)$new); case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: if ($old === null) { return true; } return ((bool)$old !== (bool)$new); } return parent::transactionHasEffect($object, $xaction); } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_QUESTION: return $object->getQuestion(); case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: return $object->getResponseVisibility(); case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: return $object->getShuffle(); case PhabricatorSlowvoteTransaction::TYPE_CLOSE: return $object->getIsClosed(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_QUESTION: case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: case PhabricatorSlowvoteTransaction::TYPE_CLOSE: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorSlowvoteTransaction::TYPE_QUESTION: $object->setQuestion($xaction->getNewValue()); break; case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); break; case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: $object->setResponseVisibility($xaction->getNewValue()); break; case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: $object->setShuffle($xaction->getNewValue()); break; case PhabricatorSlowvoteTransaction::TYPE_CLOSE: $object->setIsClosed((int)$xaction->getNewValue()); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + public function getMailTagsMap() { + return array( + PhabricatorSlowvoteTransaction::MAILTAG_DETAILS => + pht('Someone changes the poll details.'), + PhabricatorSlowvoteTransaction::MAILTAG_RESPONSES => + pht('Someone votes on a poll.'), + PhabricatorSlowvoteTransaction::MAILTAG_OTHER => + pht('Other poll activity not listed above occurs.'), + ); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $monogram = $object->getMonogram(); + $name = $object->getQuestion(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("{$monogram}: {$name}") + ->addHeader('Thread-Topic', $monogram); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + $description = $object->getDescription(); + + if (strlen($description)) { + $body->addTextSection( + pht('SLOWVOTE DESCRIPTION'), + $object->getDescription()); + } + + $body->addLinkSection( + pht('SLOWVOTE DETAIL'), + PhabricatorEnv::getProductionURI('/'.$object->getMonogram())); + + return $body; + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + return array( + $object->getAuthorPHID(), + $this->requireActor()->getPHID(), + ); + } + protected function getMailSubjectPrefix() { + return '[Slowvote]'; + } + + protected function buildReplyHandler(PhabricatorLiskDAO $object) { + return id(new PhabricatorSlowvoteReplyHandler()) + ->setMailReceiver($object); + } + + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + } diff --git a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php new file mode 100644 index 0000000000..78e608231f --- /dev/null +++ b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php @@ -0,0 +1,28 @@ +setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + } + + protected function getTransactionReplyHandler() { + return new PhabricatorSlowvoteReplyHandler(); + } + +} diff --git a/src/applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php b/src/applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php new file mode 100644 index 0000000000..b67327e506 --- /dev/null +++ b/src/applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php @@ -0,0 +1,16 @@ +setViewer($actor) ->withClasses(array('PhabricatorSlowvoteApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorSlowvoteDefaultViewCapability::CAPABILITY); return id(new PhabricatorSlowvotePoll()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'question' => 'text255', 'responseVisibility' => 'uint32', 'shuffle' => 'uint32', 'method' => 'uint32', 'description' => 'text', 'isClosed' => 'bool', + 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorSlowvotePollPHIDType::TYPECONST); } public function getOptions() { return $this->assertAttached($this->options); } public function attachOptions(array $options) { assert_instances_of($options, 'PhabricatorSlowvoteOption'); $this->options = $options; return $this; } public function getChoices() { return $this->assertAttached($this->choices); } public function attachChoices(array $choices) { assert_instances_of($choices, 'PhabricatorSlowvoteChoice'); $this->choices = $choices; return $this; } public function getViewerChoices(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->viewerChoices, $viewer->getPHID()); } public function attachViewerChoices(PhabricatorUser $viewer, array $choices) { if ($this->viewerChoices === self::ATTACHABLE) { $this->viewerChoices = array(); } assert_instances_of($choices, 'PhabricatorSlowvoteChoice'); $this->viewerChoices[$viewer->getPHID()] = $choices; return $this; } + public function getMonogram() { + return 'V'.$this->getID(); + } + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorSlowvoteEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorSlowvoteTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht('The author of a poll can always view and edit it.'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getAuthorPHID()); } /* -( PhabricatorDestructableInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID = %d', $this->getID()); foreach ($choices as $choice) { $choice->delete(); } $options = id(new PhabricatorSlowvoteOption())->loadAllWhere( 'pollID = %d', $this->getID()); foreach ($options as $option) { $option->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )--------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php index 9abf1f5801..e45fdb013f 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php @@ -1,156 +1,257 @@ getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: case self::TYPE_RESPONSES: case self::TYPE_SHUFFLE: case self::TYPE_CLOSE: return ($old === null); } return parent::shouldHide(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_QUESTION: if ($old === null) { return pht( '%s created this poll.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the poll question from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_DESCRIPTION: return pht( '%s updated the description for this poll.', $this->renderHandleLink($author_phid)); case self::TYPE_RESPONSES: // TODO: This could be more detailed return pht( '%s changed who can see the responses.', $this->renderHandleLink($author_phid)); case self::TYPE_SHUFFLE: if ($new) { return pht( '%s made poll responses appear in a random order.', $this->renderHandleLink($author_phid)); } else { return pht( '%s made poll responses appear in a fixed order.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_CLOSE: if ($new) { return pht( '%s closed this poll.', $this->renderHandleLink($author_phid)); } else { return pht( '%s reopened this poll.', $this->renderHandleLink($author_phid)); } break; } return parent::getTitle(); } + public function getTitleForFeed() { + $author_phid = $this->getAuthorPHID(); + $object_phid = $this->getObjectPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $type = $this->getTransactionType(); + switch ($type) { + case self::TYPE_QUESTION: + if ($old === null) { + return pht( + '%s created %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + } else { + return pht( + '%s renamed %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + break; + case self::TYPE_DESCRIPTION: + if ($old === null) { + return pht( + '%s set the description of %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + } else { + return pht( + '%s edited the description of %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + break; + case self::TYPE_RESPONSES: + // TODO: This could be more detailed + return pht( + '%s changed who can see the responses of %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + case self::TYPE_SHUFFLE: + if ($new) { + return pht( + '%s made %s responses appear in a random order.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + } else { + return pht( + '%s made %s responses appear in a fixed order.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + case self::TYPE_CLOSE: + if ($new) { + return pht( + '%s closed %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + + } else { + return pht( + '%s reopened %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + break; + } + + return parent::getTitleForFeed(); + } + public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_QUESTION: if ($old === null) { return 'fa-plus'; } else { return 'fa-pencil'; } case self::TYPE_DESCRIPTION: case self::TYPE_RESPONSES: return 'fa-pencil'; case self::TYPE_SHUFFLE: return 'fa-refresh'; case self::TYPE_CLOSE: if ($new) { return 'fa-ban'; } else { return 'fa-pencil'; } } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_QUESTION: case self::TYPE_DESCRIPTION: case self::TYPE_RESPONSES: case self::TYPE_SHUFFLE: case self::TYPE_CLOSE: return PhabricatorTransactions::COLOR_BLUE; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } + public function getMailTags() { + $tags = parent::getMailTags(); + + switch ($this->getTransactionType()) { + case self::TYPE_QUESTION: + case self::TYPE_DESCRIPTION: + case self::TYPE_SHUFFLE: + case self::TYPE_CLOSE: + $tags[] = self::MAILTAG_DETAILS; + break; + case self::TYPE_RESPONSES: + $tags[] = self::MAILTAG_RESPONSES; + break; + default: + $tags[] = self::MAILTAG_OTHER; + break; + } + + return $tags; + } + } diff --git a/src/applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php b/src/applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php new file mode 100644 index 0000000000..20df5e06c4 --- /dev/null +++ b/src/applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php @@ -0,0 +1,29 @@ +getRule()->getAuthorPHID(); + return $this->applySubscribe(array($phid), $is_add = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Add rule author as subscriber.'); + } + +} diff --git a/src/applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php b/src/applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php new file mode 100644 index 0000000000..14f72d313a --- /dev/null +++ b/src/applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php @@ -0,0 +1,32 @@ +applySubscribe($effect->getTarget(), $is_add = true); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorMetaMTAMailableDatasource(); + } + + public function renderActionDescription($value) { + return pht('Add subscribers: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php b/src/applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php new file mode 100644 index 0000000000..6ec7b6f776 --- /dev/null +++ b/src/applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php @@ -0,0 +1,187 @@ +getAdapter(); + + $allowed_types = array( + PhabricatorPeopleUserPHIDType::TYPECONST, + PhabricatorProjectProjectPHIDType::TYPECONST, + ); + + // Evaluating "No Effect" is a bit tricky for this rule type, so just + // do it manually below. + $current = array(); + + $targets = $this->loadStandardTargets($phids, $allowed_types, $current); + if (!$targets) { + return; + } + + $phids = array_fuse(array_keys($targets)); + + // The "Add Subscribers" rule only adds subscribers who haven't previously + // unsubscribed from the object explicitly. Filter these subscribers out + // before continuing. + if ($is_add) { + $unsubscribed = $adapter->loadEdgePHIDs( + PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST); + + foreach ($unsubscribed as $phid) { + if (isset($phids[$phid])) { + $unsubscribed[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($unsubscribed) { + $this->logEffect( + self::DO_PREVIOUSLY_UNSUBSCRIBED, + array_values($unsubscribed)); + } + } + + if (!$phids) { + return; + } + + $auto = array(); + $object = $adapter->getObject(); + foreach ($phids as $phid) { + if ($object->isAutomaticallySubscribed($phid)) { + $auto[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($auto) { + $this->logEffect(self::DO_AUTOSUBSCRIBED, array_values($auto)); + } + + if (!$phids) { + return; + } + + $current = $adapter->loadEdgePHIDs( + PhabricatorObjectHasSubscriberEdgeType::EDGECONST); + + if ($is_add) { + $already = array(); + foreach ($phids as $phid) { + if (isset($current[$phid])) { + $already[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($already) { + $this->logEffect(self::DO_STANDARD_NO_EFFECT, $already); + } + } else { + $already = array(); + foreach ($phids as $phid) { + if (empty($current[$phid])) { + $already[$phid] = $phid; + unset($phids[$phid]); + } + } + + if ($already) { + $this->logEffect(self::DO_STANDARD_NO_EFFECT, $already); + } + } + + if (!$phids) { + return; + } + + if ($is_add) { + $kind = '+'; + } else { + $kind = '-'; + } + + $xaction = $adapter->newTransaction() + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) + ->setNewValue( + array( + $kind => $phids, + )); + + $adapter->queueTransaction($xaction); + + if ($is_add) { + $this->logEffect(self::DO_SUBSCRIBED, $phids); + } else { + $this->logEffect(self::DO_UNSUBSCRIBED, $phids); + } + } + + protected function getActionEffectMap() { + return array( + self::DO_PREVIOUSLY_UNSUBSCRIBED => array( + 'icon' => 'fa-minus-circle', + 'color' => 'grey', + 'name' => pht('Previously Unsubscribed'), + ), + self::DO_AUTOSUBSCRIBED => array( + 'icon' => 'fa-envelope', + 'color' => 'grey', + 'name' => pht('Automatically Subscribed'), + ), + self::DO_SUBSCRIBED => array( + 'icon' => 'fa-envelope', + 'color' => 'green', + 'name' => pht('Added Subscribers'), + ), + self::DO_UNSUBSCRIBED => array( + 'icon' => 'fa-minus-circle', + 'color' => 'green', + 'name' => pht('Removed Subscribers'), + ), + ); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_PREVIOUSLY_UNSUBSCRIBED: + return pht( + 'Declined to resubscribe %s target(s) because they previously '. + 'unsubscribed: %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_AUTOSUBSCRIBED: + return pht( + '%s automatically subscribed target(s) were not affected: %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_SUBSCRIBED: + return pht( + 'Added %s subscriber(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + case self::DO_UNSUBSCRIBED: + return pht( + 'Removed %s subscriber(s): %s.', + new PhutilNumber(count($data)), + $this->renderHandleList($data)); + } + } + + +} diff --git a/src/applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php b/src/applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php new file mode 100644 index 0000000000..5d751cb54a --- /dev/null +++ b/src/applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php @@ -0,0 +1,29 @@ +getRule()->getAuthorPHID(); + return $this->applySubscribe(array($phid), $is_add = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_NONE; + } + + public function renderActionDescription($value) { + return pht('Remove rule author as subscriber.'); + } + +} diff --git a/src/applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php b/src/applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php new file mode 100644 index 0000000000..67ece2cb72 --- /dev/null +++ b/src/applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php @@ -0,0 +1,32 @@ +applySubscribe($effect->getTarget(), $is_add = false); + } + + public function getHeraldActionStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorMetaMTAMailableDatasource(); + } + + public function renderActionDescription($value) { + return pht('Remove subscribers: %s.', $this->renderHandleList($value)); + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 0b6dd2de91..45782758f8 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1,3134 +1,3143 @@ 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; } protected function getMentionedPHIDs() { return $this->mentionedPHIDs; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function getIsPreview() { return $this->isPreview; } 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; } /** * Prevent this editor from generating email when applying transactions. * * @param bool True to disable email. * @return this */ public function setDisableEmail($disable_email) { $this->disableEmail = $disable_email; return $this; } public function getDisableEmail() { return $this->disableEmail; } 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 getTransactionTypes() { $types = array(); if ($this->object instanceof PhabricatorSubscribableInterface) { $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS; } if ($this->object instanceof PhabricatorCustomFieldInterface) { $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD; } if ($this->object instanceof HarbormasterBuildableInterface) { $types[] = PhabricatorTransactions::TYPE_BUILDABLE; } 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; } 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) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: return array_values($this->subscribers); case PhabricatorTransactions::TYPE_VIEW_POLICY: return $object->getViewPolicy(); case PhabricatorTransactions::TYPE_EDIT_POLICY: return $object->getEditPolicy(); case PhabricatorTransactions::TYPE_JOIN_POLICY: return $object->getJoinPolicy(); case PhabricatorTransactions::TYPE_SPACE: $space_phid = $object->getSpacePHID(); if ($space_phid === null) { if ($this->getIsNewObject()) { // In this case, just return `null` so we know this is the initial // transaction and it should be hidden. return 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')); } $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) { switch ($xaction->getTransactionType()) { 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_BUILDABLE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_INLINESTATE: return $xaction->getNewValue(); 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: return $this->getEdgeTransactionNewValue($xaction); 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_COMMENT: return $xaction->hasComment(); 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; } 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) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionInternalEffects($xaction); case PhabricatorTransactions::TYPE_BUILDABLE: 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) { switch ($xaction->getTransactionType()) { 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_EDGE: case PhabricatorTransactions::TYPE_BUILDABLE: 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; } } /** * 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'); $type = PhabricatorEdgeType::getByConstant($const); if ($type->shouldWriteInverseTransactions()) { $this->applyInverseEdgeTransactions( $object, $xaction, $type->getInverseEdgeConstant()); } 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(); 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()); } $xaction->setAuthorPHID($this->getActingAsPHID()); $xaction->setContentSource($this->getContentSource()); $xaction->attachViewer($actor); $xaction->attachObject($object); if ($object->getPHID()) { $xaction->setObjectPHID($object->getPHID()); } return $xaction; } protected function didApplyInternalEffects( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { return $xactions; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function setContentSourceFromRequest(AphrontRequest $request) { return $this->setContentSource( PhabricatorContentSource::newFromRequest($request)); } public function setContentSourceFromConduitRequest( ConduitAPIRequest $request) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_CONDUIT, array()); return $this->setContentSource($content_source); } public function getContentSource() { return $this->contentSource; } final public function applyTransactions( PhabricatorLiskDAO $object, array $xactions) { $this->object = $object; $this->xactions = $xactions; $this->isNewObject = ($object->getPHID() === null); $this->validateEditParameters($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 = 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); } $file_phids = $this->extractFilePHIDs($object, $xactions); if ($object->getID()) { foreach ($xactions as $xaction) { // If any of the transactions require a read lock, hold one and // reload the object. We need to do this fairly early so that the // call to `adjustTransactionValues()` (which populates old values) // is based on the synchronized state of the object, which may differ // from the state when it was originally loaded. if ($this->shouldReadLock($object, $xaction)) { $object->openTransaction(); $object->beginReadLocking(); $transaction_open = true; $read_locking = true; $object->reload(); break; } } } if ($this->shouldApplyInitialEffects($object, $xactions)) { if (!$transaction_open) { $object->openTransaction(); $transaction_open = true; } } } if ($this->shouldApplyInitialEffects($object, $xactions)) { $this->applyInitialEffects($object, $xactions); } foreach ($xactions as $xaction) { $this->adjustTransactionValues($object, $xaction); } $xactions = $this->filterTransactions($object, $xactions); if (!$xactions) { if ($read_locking) { $object->endReadLocking(); $read_locking = false; } if ($transaction_open) { $object->killTransaction(); $transaction_open = false; } return array(); } // Now that we've merged, filtered, and combined transactions, check for // required capabilities. foreach ($xactions as $xaction) { $this->requireCapabilities($object, $xaction); } $xactions = $this->sortTransactions($xactions); if ($is_preview) { $this->loadHandles($xactions); return $xactions; } $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($actor) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); if (!$transaction_open) { $object->openTransaction(); } foreach ($xactions as $xaction) { $this->applyInternalEffects($object, $xaction); } $xactions = $this->didApplyInternalEffects($object, $xactions); try { $object->save(); } catch (AphrontDuplicateKeyQueryException $ex) { $object->killTransaction(); // 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) { $xaction->setObjectPHID($object->getPHID()); if ($xaction->getComment()) { $xaction->setPHID($xaction->generatePHID()); $comment_editor->applyEdit($xaction, $xaction->getComment()); } 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; } $object->saveTransaction(); // 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()) { // If we're applying inverse edge transactions, don't trigger Herald. // From a product perspective, the current set of inverse edges (most // often, mentions) aren't things users would expect to trigger Herald. // From a technical perspective, objects loaded by the inverse editor may // not have enough data to execute rules. At least for now, just stop // Herald from executing when applying inverse edges. } 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) { $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( PhabricatorContentSource::SOURCE_HERALD, array()); $herald_editor = newv(get_class($this), array()) ->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->didApplyTransactions($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; // 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->getDisableEmail()) { if ($this->shouldSendMail($object, $xactions)) { $this->mailToPHIDs = $this->getMailTo($object); $this->mailCCPHIDs = $this->getMailCC($object); } } if ($this->shouldPublishFeedStory($object, $xactions)) { $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, )); return $xactions; } protected function didCatchDuplicateKeyException( PhabricatorLiskDAO $object, array $xactions, Exception $ex) { return; } public function publishTransactions( PhabricatorLiskDAO $object, array $xactions) { // Hook for edges or other properties that may need (re-)loading $object = $this->willPublish($object, $xactions); $messages = array(); if (!$this->getDisableEmail()) { if ($this->shouldSendMail($object, $xactions)) { $messages = $this->buildMail($object, $xactions); } } if ($this->supportsSearch()) { id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing( $object->getPHID(), $this->getSearchContextParameter($object, $xactions)); } if ($this->shouldPublishFeedStory($object, $xactions)) { $mailed = array(); foreach ($messages as $mail) { foreach ($mail->buildRecipientList() as $phid) { $mailed[$phid] = $phid; } } $this->publishFeedStory($object, $xactions, $mailed); } // 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(); } return $xactions; } protected function didApplyTransactions(array $xactions) { // Hook for subclasses. return; } /** * Determine if the editor should hold a read lock on the object while * applying a transaction. * * If the editor does not hold a lock, two editors may read an object at the * same time, then apply their changes without any synchronization. For most * transactions, this does not matter much. However, it is important for some * transactions. For example, if an object has a transaction count on it, both * editors may read the object with `count = 23`, then independently update it * and save the object with `count = 24` twice. This will produce the wrong * state: the object really has 25 transactions, but the count is only 24. * * Generally, transactions fall into one of four buckets: * * - Append operations: Actions like adding a comment to an object purely * add information to its state, and do not depend on the current object * state in any way. These transactions never need to hold locks. * - Overwrite operations: Actions like changing the title or description * of an object replace the current value with a new value, so the end * state is consistent without a lock. We currently do not lock these * transactions, although we may in the future. * - Edge operations: Edge and subscription operations have internal * synchronization which limits the damage race conditions can cause. * We do not currently lock these transactions, although we may in the * future. * - Update operations: Actions like incrementing a count on an object. * These operations generally should use locks, unless it is not * important that the state remain consistent in the presence of races. * * @param PhabricatorLiskDAO Object being updated. * @param PhabricatorApplicationTransaction Transaction being applied. * @return bool True to synchronize the edit with a lock. */ protected function shouldReadLock( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return false; } 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->getAuthorPHID()) { throw new PhabricatorApplicationTransactionStructureException( $xaction, pht( 'You can not apply transactions which already have %s!', 'authorPHIDs')); } 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(); 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))); } } } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { if ($this->getIsNewObject()) { return; } $actor = $this->requireActor(); switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: PhabricatorPolicyFilter::requireCapability( $actor, $object, PhabricatorPolicyCapability::CAN_VIEW); break; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_SPACE: PhabricatorPolicyFilter::requireCapability( $actor, $object, PhabricatorPolicyCapability::CAN_EDIT); break; } } private function buildSubscribeTransaction( PhabricatorLiskDAO $object, array $xactions, array $blocks) { if (!($object instanceof PhabricatorSubscribableInterface)) { return null; } if ($this->shouldEnableMentions($object, $xactions)) { $texts = array_mergev($blocks); $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $this->getActor(), $texts); } 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) ) { unset($phids[$key]); } else { if ($object->isAutomaticallySubscribed($phid)) { unset($phids[$key]); } } } $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)); return $xaction; } protected function getRemarkupBlocksFromTransaction( PhabricatorApplicationTransaction $transaction) { return $transaction->getRemarkupBlocks(); } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); 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. */ private 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); $blocks = array(); foreach ($xactions as $key => $xaction) { $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction); } $subscribe_xaction = $this->buildSubscribeTransaction( $object, $xactions, $blocks); 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, $blocks, $engine); foreach ($block_xactions as $xaction) { $xactions[] = $xaction; } return $xactions; } private function expandRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, $blocks, PhutilMarkupEngine $engine) { $block_xactions = $this->expandCustomRemarkupBlockTransactions( $object, $xactions, $blocks, $engine); $mentioned_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($blocks as $key => $xaction_blocks) { foreach ($xaction_blocks as $block) { $engine->markupText($block); $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, $blocks, 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) { $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); } protected 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()); } $new = $xaction->getNewValue(); $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), '%' (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); + $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) { + 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').", - $key)); + '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').", - $item)); + '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; $no_effect = array(); $has_comment = false; $any_effect = false; foreach ($xactions as $key => $xaction) { 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 (!$no_effect) { return $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->getComment()) { $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 Transactions of given type, * which may be empty if the edit does not apply any transactions of the * given type. * @return list List of * validation errors. */ protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = array(); 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_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); } private 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; } 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 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; } } $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); $targets = $this->buildReplyHandler($object) ->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); } catch (Exception $ex) { $caught = $ex; } $this->setActor($original_actor); unset($locale); if ($caught) { throw $ex; } if ($mail) { $messages[] = $mail; } } return $messages; } 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 = $this->buildMailTemplate($object); $body = $this->buildMailBody($object, $xactions); $mail_tags = $this->getMailTags($object, $xactions); $action = $this->getMailAction($object, $xactions); if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) { $this->addEmailPreferenceSectionToMailBody( $body, $object, $xactions); } $mail ->setSensitiveContent(false) ->setFrom($this->getActingAsPHID()) ->setSubjectPrefix($this->getMailSubjectPrefix()) ->setVarySubjectPrefix('['.$action.']') ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) ->setRelatedPHID($object->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->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()); } 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.')); } /** * @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) { $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($project_phids) ->withEdgeTypes(array($watcher_type)); $query->execute(); $watcher_phids = $query->getDestinationPHIDs(); 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('Capability not supported.')); } return array_mergev($phids); } /** * @task mail */ protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = new PhabricatorMetaMTAMailBody(); $body->setViewer($this->requireActor()); $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) { $headers = array(); $comments = array(); foreach ($xactions as $xaction) { if ($xaction->shouldHideForMail($xactions)) { continue; } $header = $xaction->getTitleForMail(); if ($header !== null) { $headers[] = $header; } $comment = $xaction->getBodyForMail(); if ($comment !== null) { $comments[] = $comment; } } $body->addRawSection(implode("\n", $headers)); foreach ($comments as $comment) { $body->addRemarkupSection($comment); } } /** * @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); } } } /* -( 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) { return array_unique(array_merge( $this->getMailTo($object), $this->getMailCC($object))); } /** * @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) { $xactions = mfilter($xactions, 'shouldHideForFeed', true); if (!$xactions) { return; } $related_phids = $this->feedRelatedPHIDs; $subscribed_phids = $this->feedNotifyPHIDs; $story_type = $this->getFeedStoryType(); $story_data = $this->getFeedStoryData($object, $xactions); id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) ->setStoryTime(time()) ->setStoryAuthorPHID($this->getActingAsPHID()) ->setRelatedPHIDs($related_phids) ->setPrimaryObjectPHID($object->getPHID()) ->setSubscribedPHIDs($subscribed_phids) ->setMailRecipientPHIDs($mailed_phids) ->setMailTags($this->getMailTags($object, $xactions)) ->publish(); } /* -( Search Index )------------------------------------------------------- */ /** * @task search */ protected function supportsSearch() { return false; } /** * @task search */ protected function getSearchContextParameter( PhabricatorLiskDAO $object, array $xactions) { return null; } /* -( 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); $adapter->setContentSource($this->getContentSource()); $adapter->setIsNewObject($this->getIsNewObject()); if ($this->getApplicationEmail()) { $adapter->setApplicationEmail($this->getApplicationEmail()); } $xscript = HeraldEngine::loadAndApplyRules($adapter); $this->setHeraldAdapter($adapter); $this->setHeraldTranscript($xscript); + if ($adapter instanceof HarbormasterBuildableAdapterInterface) { + HarbormasterBuildable::applyBuildPlans( + $adapter->getHarbormasterBuildablePHID(), + $adapter->getHarbormasterContainerPHID(), + $adapter->getQueuedHarbormasterBuildPlanPHIDs()); + } + return array_merge( $this->didApplyHeraldRules($object, $adapter, $xscript), $adapter->getQueuedTransactions()); } 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) { $blocks = array(); foreach ($xactions as $xaction) { $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction); } $blocks = array_mergev($blocks); $phids = array(); if ($blocks) { $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( $this->getActor(), $blocks); } foreach ($xactions as $xaction) { $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(); 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; } $editor = $node->getApplicationTransactionEditor(); $template = $node->getApplicationTransactionTemplate(); $target = $node->getApplicationTransactionObject(); if (isset($add[$node->getPHID()])) { $edge_edit_type = '+'; } else { $edge_edit_type = '-'; } $template ->setTransactionType($xaction->getTransactionType()) ->setMetadataValue('edge:type', $inverse_type) ->setNewValue( array( $edge_edit_type => array($object->getPHID() => $object->getPHID()), )); $editor ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setParentMessageID($this->getParentMessageID()) ->setIsInverseEdgeEditor(true) ->setActor($this->requireActor()) ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); $editor->applyTransactions($target, 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 Serializable editor state. * @task workers */ final private function getWorkerState() { $state = array(); foreach ($this->getAutomaticStateProperties() as $property) { $state[$property] = $this->$property; } $state += array( 'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(), 'custom' => $this->getCustomWorkerState(), ); return $state; } /** * Hook; return custom properties which need to be passed to workers. * * @return dict Custom properties. * @task workers */ protected function getCustomWorkerState() { 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 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 = idx($state, 'custom', array()); $this->loadCustomWorkerState($custom); return $this; } /** * Hook; set custom properties on the editor from data emitted by * @{method:getCustomWorkerState}. * * @param dict 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 List of properties. * @task workers */ private function getAutomaticStateProperties() { return array( 'parentMessageID', 'disableEmail', 'isNewObject', 'heraldEmailPHIDs', 'heraldForcedEmailPHIDs', 'heraldHeader', 'mailToPHIDs', 'mailCCPHIDs', 'feedNotifyPHIDs', 'feedRelatedPHIDs', ); } } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index e8811da760..c7be284ecd 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1,1272 +1,1284 @@ ignoreOnNoEffect = $ignore; return $this; } public function getIgnoreOnNoEffect() { return $this->ignoreOnNoEffect; } public function shouldGenerateOldValue() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_BUILDABLE: 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(); } public function getApplicationTransactionViewObject() { return new PhabricatorApplicationTransactionView(); } 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 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 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; } if ($this->getComment()) { $blocks[] = $this->getComment()->getContent(); } return $blocks; } public function setOldValue($value) { $this->oldValueHasBeenSet = true; $this->writeField('oldValue', $value); return $this; } public function hasOldValue() { return $this->oldValueHasBeenSet; } /* -( 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()); 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: $phids[] = ipull($old, 'dst'); $phids[] = ipull($new, 'dst'); 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; case PhabricatorTransactions::TYPE_BUILDABLE: $phid = $this->getMetadataValue('harbormaster:buildablePHID'); if ($phid) { $phids[] = array($phid); } 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-eraser'; + return 'fa-trash'; } return 'fa-comment'; case PhabricatorTransactions::TYPE_SUBSCRIBERS: - return 'fa-envelope'; + $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: return 'fa-link'; case PhabricatorTransactions::TYPE_BUILDABLE: return 'fa-wrench'; case PhabricatorTransactions::TYPE_TOKEN: return 'fa-trophy'; case PhabricatorTransactions::TYPE_SPACE: return 'fa-th-large'; } 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_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return 'green'; case HarbormasterBuildable::STATUS_FAILED: return 'red'; } break; } return null; } protected function getTransactionCustomField() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); if (!$key) { return null; } $field = PhabricatorCustomField::getObjectField( $this->getObject(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if (!$field) { return null; } $field->setViewer($this->getViewer()); return $field; } return null; } public function shouldHide() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_SPACE: if ($this->getOldValue() === null) { return true; } else { return false; } break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->shouldHideInApplicationTransactions($this); } case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return true; break; case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: $new = ipull($this->getNewValue(), 'dst'); $old = ipull($this->getOldValue(), 'dst'); $add = array_diff($new, $old); $add_value = reset($add); $add_handle = $this->getHandle($add_value); if ($add_handle->getPolicyFiltered()) { return true; } return false; break; default: break; } break; } return false; } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, only ever send mail when builds fail. We might let // you customize this later, but in most cases this is probably // completely uninteresting. return false; } 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; } // If a transaction publishes an inline comment: // // - Don't show it if there are other kinds of transactions. The // rationale here is that application mail will make the presence // of inline comments obvious enough by including them prominently // in the body. We could change this in the future if the obviousness // needs to be increased. // - If there are only inline transactions, only show the first // transaction. The rationale is that seeing multiple "added an inline // comment" transactions is not useful. if ($this->isInlineCommentTransaction()) { foreach ($xactions as $xaction) { if (!$xaction->isInlineCommentTransaction()) { return true; } } return ($this !== head($xactions)); } return $this->shouldHide(); } public function shouldHideForFeed() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, don't notify on build passes either. These are pretty // high volume and annoying, with very little present value. We // might want to turn them back on in the specific case of // build successes on the current document? return false; } 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 getTitleForMail() { return id(clone $this)->setRenderingTarget('text')->getTitle(); } 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.'); } return pht('Transaction has no effect.'); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment.', $this->renderHandleLink($author_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_SPACE: 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: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getTransactionEditString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getTransactionAddString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getTransactionRemoveString( $this->renderHandleLink($author_phid), new PhutilNumber(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 { 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_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s!', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); default: return null; } case PhabricatorTransactions::TYPE_INLINESTATE: $done = 0; $undone = 0; foreach ($new as $phid => $state) { if ($state == PhabricatorInlineCommentInterface::STATE_DONE) { $done++; } else { $undone++; } } 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; default: 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_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: 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: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $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)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getFeedAddString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getFeedRemoveString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(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_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); default: 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) { $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 getActionStrength() { if ($this->isInlineCommentTransaction()) { return 0.25; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 0.5; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $old = $this->getOldValue(); $new = $this->getNewValue(); $add = array_diff($old, $new); $rem = array_diff($new, $old); // If this action is the actor subscribing or unsubscribing themselves, // it is less interesting. In particular, if someone makes a comment and // also implicitly subscribes themselves, we should treat the // transaction group as "comment", not "subscribe". In this specific // case (one affected user, and that affected user it the actor), // decrease the action strength. if ((count($add) + count($rem)) != 1) { // Not exactly one CC change. break; } $affected_phid = head(array_merge($add, $rem)); if ($affected_phid != $this->getAuthorPHID()) { // Affected user is someone else. break; } // Make this weaker than TYPE_COMMENT. return 0.25; } 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'); case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return pht('Build Passed'); case HarbormasterBuildable::STATUS_FAILED: return pht('Build Failed'); default: return pht('Build Status'); } 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 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) { require_celerity_resource('differential-changeset-view-css'); $view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) ->setUser($viewer) ->setOldText($old) ->setNewText($new); return $view->render(); } 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 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(); } 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; } } 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); } /* -( 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.'); } /* -( 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. } 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/uiexample/application/PhabricatorUIExamplesApplication.php b/src/applications/uiexample/application/PhabricatorUIExamplesApplication.php index c347a101e9..3b3d38ef14 100644 --- a/src/applications/uiexample/application/PhabricatorUIExamplesApplication.php +++ b/src/applications/uiexample/application/PhabricatorUIExamplesApplication.php @@ -1,46 +1,50 @@ array( '' => 'PhabricatorUIExampleRenderController', 'view/(?P[^/]+)/' => 'PhabricatorUIExampleRenderController', ), ); } } diff --git a/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php b/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php index 00dd4daf41..32c60de1c5 100644 --- a/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php +++ b/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php @@ -1,65 +1,60 @@ class = idx($data, 'class'); - } - - public function processRequest() { + public function handleRequest(AphrontRequest $request) { + $id = $request->getURIData('class'); $classes = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorUIExample') ->loadObjects(); $classes = msort($classes, 'getName'); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI('view/'))); foreach ($classes as $class => $obj) { $name = $obj->getName(); $nav->addFilter($class, $name); } - $selected = $nav->selectFilter($this->class, head_key($classes)); + $selected = $nav->selectFilter($id, head_key($classes)); $example = $classes[$selected]; $example->setRequest($this->getRequest()); $result = $example->renderExample(); if ($result instanceof AphrontResponse) { // This allows examples to generate dialogs, etc., for demonstration. return $result; } require_celerity_resource('phabricator-ui-example-css'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($example->getName()); $note = id(new PHUIInfoView()) ->setTitle(pht('%s (%s)', $example->getName(), get_class($example))) ->appendChild($example->getDescription()) ->setSeverity(PHUIInfoView::SEVERITY_NODATA); $nav->appendChild( array( $crumbs, $note, $result, )); return $this->buildApplicationPage( $nav, array( 'title' => $example->getName(), )); } } diff --git a/src/applications/uiexample/examples/PHUITimelineExample.php b/src/applications/uiexample/examples/PHUITimelineExample.php index d3df3ded71..661d2e6899 100644 --- a/src/applications/uiexample/examples/PHUITimelineExample.php +++ b/src/applications/uiexample/examples/PHUITimelineExample.php @@ -1,205 +1,206 @@ PHUITimelineView')); } public function renderExample() { $request = $this->getRequest(); $user = $request->getUser(); $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($user->getPHID())) ->executeOne(); $designer = id(new PHUIBadgeMiniView()) ->setIcon('fa-camera-retro') ->setHeader(pht('Designer')) ->setQuality(PHUIBadgeView::EPIC); $admin = id(new PHUIBadgeMiniView()) ->setIcon('fa-user') ->setHeader(pht('Administrator')) ->setQuality(PHUIBadgeView::RARE); $events = array(); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('A major event.')) ->appendChild(pht('This is a major timeline event.')) ->addBadge($designer) ->addBadge($admin); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-heart') ->setTitle(pht('A minor event.')); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-comment') ->appendChild(pht('A major event with no title.')); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-star') ->setTitle(pht('Another minor event.')); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('Major Red Event')) ->setIcon('fa-heart-o') ->appendChild(pht('This event is red!')) ->setColor(PhabricatorTransactions::COLOR_RED) ->addBadge($designer); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-female') ->setTitle(pht('Minor Red Event')) ->setColor(PhabricatorTransactions::COLOR_RED); $events[] = id(new PHUITimelineEventView()) ->setIcon('fa-refresh') ->setUserHandle($handle) ->setTitle(pht('Minor Not-Red Event')) ->setColor(PhabricatorTransactions::COLOR_GREEN); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-calendar-o') ->setTitle(pht('Minor Red Event')) ->setColor(PhabricatorTransactions::COLOR_RED); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-check') ->setTitle(pht('Historically Important Action')) ->setColor(PhabricatorTransactions::COLOR_BLACK) ->setReallyMajorEvent(true); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-circle-o') ->setTitle(pht('Major Green Disagreement Action')) ->appendChild(pht('This event is green!')) ->setColor(PhabricatorTransactions::COLOR_GREEN); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setIcon('fa-tag') ->setTitle(str_repeat(pht('Long Text Title').' ', 64)) ->appendChild(str_repeat(pht('Long Text Body').' ', 64)) ->setColor(PhabricatorTransactions::COLOR_ORANGE); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(str_repeat('LongTextEventNoSpaces', 1024)) ->appendChild(str_repeat('LongTextNoSpaces', 1024)) ->setColor(PhabricatorTransactions::COLOR_RED); $colors = array( PhabricatorTransactions::COLOR_RED, PhabricatorTransactions::COLOR_ORANGE, PhabricatorTransactions::COLOR_YELLOW, PhabricatorTransactions::COLOR_GREEN, PhabricatorTransactions::COLOR_SKY, PhabricatorTransactions::COLOR_BLUE, PhabricatorTransactions::COLOR_INDIGO, PhabricatorTransactions::COLOR_VIOLET, PhabricatorTransactions::COLOR_GREY, PhabricatorTransactions::COLOR_BLACK, ); $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('Colorless')) ->setIcon('fa-lock'); foreach ($colors as $color) { $events[] = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht("Color '%s'", $color)) ->setIcon('fa-paw') ->setColor($color); } $vhandle = $handle->renderLink(); $group_event = id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s went to the store.', $vhandle)); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s bought an apple.', $vhandle)) ->setColor('green') ->setIcon('fa-apple')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s bought a banana.', $vhandle)) ->setColor('yellow') ->setIcon('fa-check')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s bought a cherry.', $vhandle)) ->setColor('red') ->setIcon('fa-check')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s paid for his goods.', $vhandle))); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s returned home.', $vhandle)) ->setIcon('fa-home') ->setColor('blue')); $group_event->addEventToGroup( id(new PHUITimelineEventView()) ->setUserHandle($handle) ->setTitle(pht('%s related on his adventures.', $vhandle)) ->appendChild( pht( 'Today, I went to the store. I bought an apple. I bought a '. 'banana. I bought a cherry. I paid for my goods, then I returned '. 'home.'))); $events[] = $group_event; $anchor = 0; foreach ($events as $group) { foreach ($group->getEventGroup() as $event) { $event->setUser($user); $event->setDateCreated(time() + ($anchor * 60 * 8)); $event->setAnchor(++$anchor); } } $timeline = id(new PHUITimelineView()); + $timeline->setUser($user); foreach ($events as $event) { $timeline->addEvent($event); } return $timeline; } } diff --git a/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php b/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php deleted file mode 100644 index fd9b422a26..0000000000 --- a/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php +++ /dev/null @@ -1,36 +0,0 @@ - 'border: 1px solid blue; padding: 20px;', - ), - 'Mary, mary quite contrary.'); - - $side = phutil_tag( - 'div', - array( - 'style' => 'border: 1px solid red; padding: 20px;', - ), - 'How does your garden grow?'); - - - $content = id(new AphrontTwoColumnView()) - ->setMainColumn($main) - ->setSideColumn($side); - - return $content; - } -} diff --git a/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php b/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php index 7f3b155cdd..85815d8589 100644 --- a/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php +++ b/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php @@ -1,63 +1,57 @@ phid = $data['phid']; - } - - public function processRequest() { - $request = $this->getRequest(); + public function handleRequest(AphrontRequest $request) { + $phid = $request->getURIData('phid'); $file = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) - ->withPHIDs(array($this->phid)) + ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $data = $file->loadFileData(); try { $data = phutil_json_decode($data); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Failed to unserialize XHProf profile!'), $ex); } $symbol = $request->getStr('symbol'); $is_framed = $request->getBool('frame'); if ($symbol) { $view = new PhabricatorXHProfProfileSymbolView(); $view->setSymbol($symbol); } else { $view = new PhabricatorXHProfProfileTopLevelView(); $view->setFile($file); $view->setLimit(100); } $view->setBaseURI($request->getRequestURI()->getPath()); $view->setIsFramed($is_framed); $view->setProfileData($data); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('%s Profile', $symbol)); return $this->buildStandardPageResponse( array($crumbs, $view), array( 'title' => pht('Profile'), 'frame' => $is_framed, )); } } diff --git a/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php b/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php index 98f20436e4..65ca5593f3 100644 --- a/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php +++ b/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php @@ -1,99 +1,97 @@ view = idx($data, 'view', 'all'); - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $view = $request->getURIData('view'); - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + if (!$view) { + $view = 'all'; + } $pager = new PHUIPagerView(); $pager->setOffset($request->getInt('page')); - switch ($this->view) { + switch ($view) { case 'sampled': $clause = 'sampleRate > 0'; $show_type = false; break; case 'my-runs': $clause = qsprintf( id(new PhabricatorXHProfSample())->establishConnection('r'), 'sampleRate = 0 AND userPHID = %s', $request->getUser()->getPHID()); $show_type = false; break; case 'manual': $clause = 'sampleRate = 0'; $show_type = false; break; case 'all': default: $clause = '1 = 1'; $show_type = true; break; } $samples = id(new PhabricatorXHProfSample())->loadAllWhere( '%Q ORDER BY id DESC LIMIT %d, %d', $clause, $pager->getOffset(), $pager->getPageSize() + 1); $samples = $pager->sliceResults($samples); $pager->setURI($request->getRequestURI(), 'page'); $list = new PHUIObjectItemListView(); foreach ($samples as $sample) { $file_phid = $sample->getFilePHID(); $item = id(new PHUIObjectItemView()) ->setObjectName($sample->getID()) ->setHeader($sample->getRequestPath()) ->setHref($this->getApplicationURI('profile/'.$file_phid.'/')) ->addAttribute( number_format($sample->getUsTotal())." \xCE\xBCs"); if ($sample->getController()) { $item->addAttribute($sample->getController()); } $item->addAttribute($sample->getHostName()); $rate = $sample->getSampleRate(); if ($rate == 0) { $item->addIcon('flag-6', pht('Manual Run')); } else { $item->addIcon('flag-7', pht('Sampled (1/%d)', $rate)); } $item->addIcon( 'none', - phabricator_datetime($sample->getDateCreated(), $user)); + phabricator_datetime($sample->getDateCreated(), $viewer)); $list->addItem($item); } $list->setPager($pager); $list->setNoDataString(pht('There are no profiling samples.')); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('XHProf Samples')); return $this->buildApplicationPage( array($crumbs, $list), array( 'title' => pht('XHProf Samples'), )); } } diff --git a/src/docs/user/userguide/harbormaster.diviner b/src/docs/user/userguide/harbormaster.diviner new file mode 100644 index 0000000000..23ed15d5e4 --- /dev/null +++ b/src/docs/user/userguide/harbormaster.diviner @@ -0,0 +1,230 @@ +@title Harbormaster User Guide +@group userguide + +Guide to Harbormaster, a build and continuous integration application. + +Overview +======== + +WARNING: Harbormaster is still very rough. Read this document carefully to +understand what it can and can not do and what to expect in the future. + +The Harbormaster application provides build and continuous integration support +for Phabricator. + +Harbormaster is not a mature application. You should expect it to have major +missing capabilities and to change substantially over time. The current version +of Harbormaster can perform some basic build tasks, but has many limitations +and is not a complete build platform. + +In particular, you should be aware of these common limitations: + + - **Creating Build Plans**: Harbormaster ships with only very basic, crude + tools for writing build plans. There are no default integrations with + `arc unit` or systems like Jenkins. Build plans are likely to change + substantially over time. + - **Triggering Builds**: Harbormaster can only trigger builds through Herald + rules. It can not currently run periodic builds. + - **Executing Builds**: Harbormaster can only execute builds in a remote + system, like Jenkins. It can not currently host builds. + - **Change Handoff**: Change handoff is covered in rough edges and tradeoffs. + + +Harbormaster Basics +=================== + +Use Harbormaster to run builds or tests on code reviews and commits. In general, +the Harbormaster workflow looks like this today: + + - You create a new "Build Plan" which describes how to build a project (which + tests to run, which commands to execute, etc). + - You configure Harbormaster to trigger the plan when relevant code reviews + are created or relevant commits are pushed or discovered. + - Harbormaster executes the plan and reports the results, allowing you to see + if a change or commit breaks tests. + +The remainder of this document walks through these steps in more detail. + + +Concepts and Terminology +======================== + +Harbormaster uses these concepts to describe builds: + + - **Build Step**: Describes a single step in a build process, like running a + command. + - **Build Plan**: A collection of build steps which describe a build process. + You'll create build plans to tell Harbormaster which commands it needs to + run to perform a build. + - **Buildable**: A reference to an object from another application which can + have builds run against it. In the upstream, code reviews (from + Differential) and commits (from Diffusion) are buildable. + - **Build**: Created by running a build plan against a buildable. Collects + results from running build commands and shows build progress, status and + results. A build describes what happened when an entire build plan was + run. + - **Build Target**: Builds are made up of build targets, which are created + automatically when Harbormaster runs the individual steps in a build. A + build target describes what happened when a specific build step was run. + + +Creating a Build Plan +===================== + +NOTE: Build plans are currently crude and subject to change in future versions +of Harbormaster. + +A build plan tells Harbormaster how to run a build: which commands to run, +services to call, and so on. Builds start with a build plan. + +To create a build plan, navigate to {nav Harbormaster > Manage Build Plans > +New Build Plan}. + +Build plans are composed of "Build Steps". Each step describes an individual +action (like running a command) and the sequence of steps as a whole comprise +the plan. For example, you might want to run one command to build a binary, +then a second command to execute unit tests. Add steps to your build plan +with {nav Add Build Step}. + +Currently, the only useful type of build step is "Make HTTP Request", which you +can use to make a call to an external build system like Jenkins. Today, most +plans should therefor look something like this: + + - Use a "Make HTTP Request" step to tell Jenkins or some other similar + external build system about the code. + - Have the build step "Wait for Message" after the external system is + notified. + - Write custom code on the build server to respond to the request, run a + build, then report the results back to Phabricator by calling the + `harbormaster.sendmessage` Conduit API. + +You'll need to write a nontrivial amount of code to get this working today. +In the future, Harbormaster will become more powerful and have more builtin +support for interacting with build systems. + + +Triggering Builds +================= + +NOTE: Harbormaster can not currently watch a branch (like "build 'master' every +time it changes") or run periodic builds (like "build every hour"). These +capabilities may be added in the future. + +You can run builds manually by using {nav Run Plan Manually} from the detail +screen of a build plan. This will execute a manual build immediately, and can +be used to test that plans work properly. + +To trigger a build automatically, write a Herald rule which executes the "Run +build plans" action. The simplest rule would just use the "Always" condition +and run a single build plan, but you can use more complex conditions to control +which plans run on which code. + +This action is available for commits and revisions, as either can be built +with Harbormaster. This action is only available for "Project" or "Global" +rules. + +Change Handoff +============== + +NOTE: Change handoff is currently very rough. It may improve in the future. + +If you want to build code reviews in an external system, it will need to be +able to construct a working copy with the changes before it can build them. + +There are three ways to do this: + + - **Automatic Staging Areas**: Recommended. This is the simplest and + cleanest way to hand changes to an external build system. + - **Manual Staging Areas**: Recommended if you can not use automatic + staging areas. This is a simple way to hand changes to an external build + system, but not as clean as automatic staging areas. + - **`arc patch`**: Not recommended. This mechanism is the most difficult to + configure and debug, and is not nearly as reliable as handoff via staging + areas. + +With staging areas, `arc` pushes a copy of the local changes somewhere as a +side effect of running `arc diff`. In Git, it pushes changes to a tag like +`phabricator/diff/123` in a designated remote. + +The build system can then interact with this copy using normal VCS commands. +This is simpler to configure, use, troubleshoot and work with than `arc patch`. + +With `arc patch`, the build system downloads patches from Phabricator and +applies them to a local working copy. This is more complex and more error-prone +than staging areas. + +**Automatic Staging Areas**: This is the recommended mechanism for change +handoff. This mechanism has not been built yet, so you can not use it. + +**Manual Staging Areas**: If you can not use automatic staging areas, manual +staging areas are the next best approach. Manual staging areas are only +supported under Git, but work with both hosted and imported repositories. + +Manual staging areas work like this: + + - You configure a staging area for the repository you want to be able to + run builds for. A staging area is just a remote repository that you're + designating for temporary storage. + - Once a staging area is configured, `arc diff` will automatically push a + copy of the changes to the staging area as a side effect when creating + and updating reviews. + - Your build system can pull changes directly from the configured staging + area. + +Configure a staging area by navigating to {nav Diffusion > +(Choose a Repository) > Edit Repository > Edit Staging}. You'll enter the +remote URI of a repository to use as a staging area, and `arc diff` will push +changes to tags like `phabricator/diff/123`. + +There are several ways to select a staging area: + + - You can use the repository itself as its own staging area, but this will + clog it up with a lot of tags that users probably don't care about. This is + simplest to configure but will be disruptive and potentially confusing to + users. + - You can create a single staging repository and have all other + repositories use it as a staging area. This is simple to configure and + won't disrupt or confuse users, but you won't be able to set granular + permissions on the staging repository: all the staged changes in a + repository are visible to anyone who has access to the repository, even if + they came from a repository that the viewer does not have access to. + - You can create a staging repository for each standard repository. This will + give you the most control, but is also the most time consuming to configure. + - You can use a hybrid approach and have several staging repositories, each + of which is used for one or more standard repositories. This will let you + strike a balance between setup costs and granularity. + - Using automatic staging areas avoids all this complexity by using the + repository as its own staging area but hiding the tags from users. + +Once you've configured a staging area, have your build system clone the staging +area repository and do a checkout of the relevant tag in order to perform a +build. + +**`arc patch`**: You can also have the build system pull changes out of +Phabricator as patches and apply them with `arc patch`. This mechanism is the +most complex to configure and debug, and is much less reliable than using +staging areas. It is not recommended. + +To use `arc patch`-based handoff, install PHP on your build server and set up +`arc`. Create a "bot" user for your build system and generate a Conduit token +in {nav Settings > Conduit API Tokens}. Then have your build system clone the +repository and run `arc patch` to apply the changes: + + $ arc patch --conduit-token --diff + +This will usually work, but is more complex and less reliable than using a +staging area. + + +Troubleshooting +=============== + +You can troubleshoot Harbormaster by using `bin/harbormaster` from the command +line. Run it as `bin/harbormaster help` for details. + +In particular, you can run manual builds in the foreground from the CLI to see +more details about what they're doing: + + phabricator/ $ ./bin/harbormaster build D123 --plan 456 --trace + +This may help you understand or debug issues with a build plan. diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index d0466fbb7d..98d062766b 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1,1262 +1,1366 @@ array( 'No daemon with id %s exists!', 'No daemons with ids %s exist!', ), 'These %d configuration value(s) are related:' => array( 'This configuration value is related:', 'These configuration values are related:', ), '%s Task(s)' => array('Task', 'Tasks'), '%s ERROR(S)' => array('ERROR', 'ERRORS'), '%d Error(s)' => array('%d Error', '%d Errors'), '%d Warning(s)' => array('%d Warning', '%d Warnings'), '%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'), '%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'), '%d Detail(s)' => array('%d Detail', '%d Details'), '(%d line(s))' => array('(%d line)', '(%d lines)'), '%d line(s)' => array('%d line', '%d lines'), '%d path(s)' => array('%d path', '%d paths'), '%d diff(s)' => array('%d diff', '%d diffs'), + '%d Answer(s)' => array('%d Answer', '%d Answers'), + '%s DIFF LINK(S)' => array('DIFF LINK', 'DIFF LINKS'), 'You successfully created %d diff(s).' => array( 'You successfully created %d diff.', 'You successfully created %d diffs.', ), 'Diff creation failed; see body for %s error(s).' => array( 'Diff creation failed; see body for error.', 'Diff creation failed; see body for errors.', ), 'There are %d raw fact(s) in storage.' => array( 'There is %d raw fact in storage.', 'There are %d raw facts in storage.', ), 'There are %d aggregate fact(s) in storage.' => array( 'There is %d aggregate fact in storage.', 'There are %d aggregate facts in storage.', ), '%d Commit(s) Awaiting Audit' => array( '%d Commit Awaiting Audit', '%d Commits Awaiting Audit', ), '%d Problem Commit(s)' => array( '%d Problem Commit', '%d Problem Commits', ), '%d Review(s) Blocking Others' => array( '%d Review Blocking Others', '%d Reviews Blocking Others', ), '%d Review(s) Need Attention' => array( '%d Review Needs Attention', '%d Reviews Need Attention', ), '%d Review(s) Waiting on Others' => array( '%d Review Waiting on Others', '%d Reviews Waiting on Others', ), '%d Active Review(s)' => array( '%d Active Review', '%d Active Reviews', ), '%d Flagged Object(s)' => array( '%d Flagged Object', '%d Flagged Objects', ), '%d Object(s) Tracked' => array( '%d Object Tracked', '%d Objects Tracked', ), '%d Assigned Task(s)' => array( '%d Assigned Task', '%d Assigned Tasks', ), 'Show %d Lint Message(s)' => array( 'Show %d Lint Message', 'Show %d Lint Messages', ), 'Hide %d Lint Message(s)' => array( 'Hide %d Lint Message', 'Hide %d Lint Messages', ), 'This is a binary file. It is %s byte(s) in length.' => array( 'This is a binary file. It is %s byte in length.', 'This is a binary file. It is %s bytes in length.', ), '%d Action(s) Have No Effect' => array( 'Action Has No Effect', 'Actions Have No Effect', ), '%d Action(s) With No Effect' => array( 'Action With No Effect', 'Actions With No Effect', ), 'Some of your %d action(s) have no effect:' => array( 'One of your actions has no effect:', 'Some of your actions have no effect:', ), 'Apply remaining %d action(s)?' => array( 'Apply remaining action?', 'Apply remaining actions?', ), 'Apply %d Other Action(s)' => array( 'Apply Remaining Action', 'Apply Remaining Actions', ), 'The %d action(s) you are taking have no effect:' => array( 'The action you are taking has no effect:', 'The actions you are taking have no effect:', ), '%s edited member(s), added %d: %s; removed %d: %s.' => '%s edited members, added: %3$s; removed: %5$s.', '%s added %s member(s): %s.' => array( array( '%s added a member: %3$s.', '%s added members: %3$s.', ), ), '%s removed %s member(s): %s.' => array( array( '%s removed a member: %3$s.', '%s removed members: %3$s.', ), ), '%s edited project(s), added %s: %s; removed %s: %s.' => '%s edited projects, added: %3$s; removed: %5$s.', '%s added %s project(s): %s.' => array( array( '%s added a project: %3$s.', '%s added projects: %3$s.', ), ), '%s removed %s project(s): %s.' => array( array( '%s removed a project: %3$s.', '%s removed projects: %3$s.', ), ), '%s merged %d task(s): %s.' => array( array( '%s merged a task: %3$s.', '%s merged tasks: %3$s.', ), ), '%s merged %d task(s) %s into %s.' => array( array( '%s merged %3$s into %4$s.', '%s merged tasks %3$s into %4$s.', ), ), '%s added %s voting user(s): %s.' => array( array( '%s added a voting user: %3$s.', '%s added voting users: %3$s.', ), ), '%s removed %s voting user(s): %s.' => array( array( '%s removed a voting user: %3$s.', '%s removed voting users: %3$s.', ), ), '%s added %s blocking task(s): %s.' => array( array( '%s added a blocking task: %3$s.', '%s added blocking tasks: %3$s.', ), ), '%s added %s blocked task(s): %s.' => array( array( '%s added a blocked task: %3$s.', '%s added blocked tasks: %3$s.', ), ), '%s removed %s blocking task(s): %s.' => array( array( '%s removed a blocking task: %3$s.', '%s removed blocking tasks: %3$s.', ), ), '%s removed %s blocked task(s): %s.' => array( array( '%s removed a blocked task: %3$s.', '%s removed blocked tasks: %3$s.', ), ), '%s added %s blocking task(s) for %s: %s.' => array( array( '%s added a blocking task for %3$s: %4$s.', '%s added blocking tasks for %3$s: %4$s.', ), ), '%s added %s blocked task(s) for %s: %s.' => array( array( '%s added a blocked task for %3$s: %4$s.', '%s added blocked tasks for %3$s: %4$s.', ), ), '%s removed %s blocking task(s) for %s: %s.' => array( array( '%s removed a blocking task for %3$s: %4$s.', '%s removed blocking tasks for %3$s: %4$s.', ), ), '%s removed %s blocked task(s) for %s: %s.' => array( array( '%s removed a blocked task for %3$s: %4$s.', '%s removed blocked tasks for %3$s: %4$s.', ), ), '%s edited blocking task(s), added %s: %s; removed %s: %s.' => '%s edited blocking tasks, added: %3$s; removed: %5$s.', '%s edited blocking task(s) for %s, added %s: %s; removed %s: %s.' => '%s edited blocking tasks for %s, added: %4$s; removed: %6$s.', '%s edited blocked task(s), added %s: %s; removed %s: %s.' => '%s edited blocked tasks, added: %3$s; removed: %5$s.', '%s edited blocked task(s) for %s, added %s: %s; removed %s: %s.' => '%s edited blocked tasks for %s, added: %4$s; removed: %6$s.', '%s edited answer(s), added %s: %s; removed %d: %s.' => '%s edited answers, added: %3$s; removed: %5$s.', '%s added %s answer(s): %s.' => array( array( '%s added an answer: %3$s.', '%s added answers: %3$s.', ), ), '%s removed %s answer(s): %s.' => array( array( '%s removed a answer: %3$s.', '%s removed answers: %3$s.', ), ), '%s edited question(s), added %s: %s; removed %s: %s.' => '%s edited questions, added: %3$s; removed: %5$s.', '%s added %s question(s): %s.' => array( array( '%s added a question: %3$s.', '%s added questions: %3$s.', ), ), '%s removed %s question(s): %s.' => array( array( '%s removed a question: %3$s.', '%s removed questions: %3$s.', ), ), '%s edited mock(s), added %s: %s; removed %s: %s.' => '%s edited mocks, added: %3$s; removed: %5$s.', '%s added %s mock(s): %s.' => array( array( '%s added a mock: %3$s.', '%s added mocks: %3$s.', ), ), '%s removed %s mock(s): %s.' => array( array( '%s removed a mock: %3$s.', '%s removed mocks: %3$s.', ), ), '%s added %s task(s): %s.' => array( array( '%s added a task: %3$s.', '%s added tasks: %3$s.', ), ), '%s removed %s task(s): %s.' => array( array( '%s removed a task: %3$s.', '%s removed tasks: %3$s.', ), ), '%s edited file(s), added %s: %s; removed %s: %s.' => '%s edited files, added: %3$s; removed: %5$s.', '%s added %s file(s): %s.' => array( array( '%s added a file: %3$s.', '%s added files: %3$s.', ), ), '%s removed %s file(s): %s.' => array( array( '%s removed a file: %3$s.', '%s removed files: %3$s.', ), ), '%s edited contributor(s), added %s: %s; removed %s: %s.' => '%s edited contributors, added: %3$s; removed: %5$s.', '%s added %s contributor(s): %s.' => array( array( '%s added a contributor: %3$s.', '%s added contributors: %3$s.', ), ), '%s removed %s contributor(s): %s.' => array( array( '%s removed a contributor: %3$s.', '%s removed contributors: %3$s.', ), ), '%s edited %s reviewer(s), added %s: %s; removed %s: %s.' => '%s edited reviewers, added: %4$s; removed: %6$s.', '%s edited %s reviewer(s) for %s, added %s: %s; removed %s: %s.' => '%s edited reviewers for %3$s, added: %5$s; removed: %7$s.', '%s added %s reviewer(s): %s.' => array( array( '%s added a reviewer: %3$s.', '%s added reviewers: %3$s.', ), ), '%s removed %s reviewer(s): %s.' => array( array( '%s removed a reviewer: %3$s.', '%s removed reviewers: %3$s.', ), ), '%d other(s)' => array( '1 other', '%d others', ), '%s edited subscriber(s), added %d: %s; removed %d: %s.' => '%s edited subscribers, added: %3$s; removed: %5$s.', '%s added %d subscriber(s): %s.' => array( array( '%s added a subscriber: %3$s.', '%s added subscribers: %3$s.', ), ), '%s removed %d subscriber(s): %s.' => array( array( '%s removed a subscriber: %3$s.', '%s removed subscribers: %3$s.', ), ), '%s edited watcher(s), added %s: %s; removed %d: %s.' => '%s edited watchers, added: %3$s; removed: %5$s.', '%s added %s watcher(s): %s.' => array( array( '%s added a watcher: %3$s.', '%s added watchers: %3$s.', ), ), '%s removed %s watcher(s): %s.' => array( array( '%s removed a watcher: %3$s.', '%s removed watchers: %3$s.', ), ), '%s edited participant(s), added %d: %s; removed %d: %s.' => '%s edited participants, added: %3$s; removed: %5$s.', '%s added %d participant(s): %s.' => array( array( '%s added a participant: %3$s.', '%s added participants: %3$s.', ), ), '%s removed %d participant(s): %s.' => array( array( '%s removed a participant: %3$s.', '%s removed participants: %3$s.', ), ), '%s edited image(s), added %d: %s; removed %d: %s.' => '%s edited images, added: %3$s; removed: %5$s', '%s added %d image(s): %s.' => array( array( '%s added an image: %3$s.', '%s added images: %3$s.', ), ), '%s removed %d image(s): %s.' => array( array( '%s removed an image: %3$s.', '%s removed images: %3$s.', ), ), '%s Line(s)' => array( '%s Line', '%s Lines', ), 'Indexing %d object(s) of type %s.' => array( 'Indexing %d object of type %s.', 'Indexing %d object of type %s.', ), 'Run these %d command(s):' => array( 'Run this command:', 'Run these commands:', ), 'Install these %d PHP extension(s):' => array( 'Install this PHP extension:', 'Install these PHP extensions:', ), 'The current Phabricator configuration has these %d value(s):' => array( 'The current Phabricator configuration has this value:', 'The current Phabricator configuration has these values:', ), 'The current MySQL configuration has these %d value(s):' => array( 'The current MySQL configuration has this value:', 'The current MySQL configuration has these values:', ), 'You can update these %d value(s) here:' => array( 'You can update this value here:', 'You can update these values here:', ), 'The current PHP configuration has these %d value(s):' => array( 'The current PHP configuration has this value:', 'The current PHP configuration has these values:', ), 'To update these %d value(s), edit your PHP configuration file.' => array( 'To update this %d value, edit your PHP configuration file.', 'To update these %d values, edit your PHP configuration file.', ), 'To update these %d value(s), edit your PHP configuration file, located '. 'here:' => array( 'To update this value, edit your PHP configuration file, located '. 'here:', 'To update these values, edit your PHP configuration file, located '. 'here:', ), 'PHP also loaded these %s configuration file(s):' => array( 'PHP also loaded this configuration file:', 'PHP also loaded these configuration files:', ), 'You have %d unresolved setup issue(s)...' => array( 'You have an unresolved setup issue...', 'You have %d unresolved setup issues...', ), '%s added %d inline comment(s).' => array( array( '%s added an inline comment.', '%s added inline comments.', ), ), '%d comment(s)' => array('%d comment', '%d comments'), '%d rejection(s)' => array('%d rejection', '%d rejections'), '%d update(s)' => array('%d update', '%d updates'), 'This configuration value is defined in these %d '. 'configuration source(s): %s.' => array( 'This configuration value is defined in this '. 'configuration source: %2$s.', 'This configuration value is defined in these %d '. 'configuration sources: %s.', ), '%d Open Pull Request(s)' => array( '%d Open Pull Request', '%d Open Pull Requests', ), 'Stale (%s day(s))' => array( 'Stale (%s day)', 'Stale (%s days)', ), 'Old (%s day(s))' => array( 'Old (%s day)', 'Old (%s days)', ), '%s Commit(s)' => array( '%s Commit', '%s Commits', ), '%s attached %d file(s): %s.' => array( array( '%s attached a file: %3$s.', '%s attached files: %3$s.', ), ), '%s detached %d file(s): %s.' => array( array( '%s detached a file: %3$s.', '%s detached files: %3$s.', ), ), '%s changed file(s), attached %d: %s; detached %d: %s.' => '%s changed files, attached: %3$s; detached: %5$s.', '%s added %s dependencie(s): %s.' => array( array( '%s added a dependency: %3$s.', '%s added dependencies: %3$s.', ), ), '%s added %s dependencie(s) for %s: %s.' => array( array( '%s added a dependency for %3$s: %4$s.', '%s added dependencies for %3$s: %4$s.', ), ), '%s removed %s dependencie(s): %s.' => array( array( '%s removed a dependency: %3$s.', '%s removed dependencies: %3$s.', ), ), '%s removed %s dependencie(s) for %s: %s.' => array( array( '%s removed a dependency for %3$s: %4$s.', '%s removed dependencies for %3$s: %4$s.', ), ), '%s added %s dependent revision(s): %s.' => array( array( '%s added a dependent revision: %3$s.', '%s added dependent revisions: %3$s.', ), ), '%s added %s dependent revision(s) for %s: %s.' => array( array( '%s added a dependent revision for %3$s: %4$s.', '%s added dependent revisions for %3$s: %4$s.', ), ), '%s removed %s dependent revision(s): %s.' => array( array( '%s removed a dependent revision: %3$s.', '%s removed dependent revisions: %3$s.', ), ), '%s removed %s dependent revision(s) for %s: %s.' => array( array( '%s removed a dependent revision for %3$s: %4$s.', '%s removed dependent revisions for %3$s: %4$s.', ), ), '%s added %s commit(s): %s.' => array( array( '%s added a commit: %3$s.', '%s added commits: %3$s.', ), ), '%s removed %s commit(s): %s.' => array( array( '%s removed a commit: %3$s.', '%s removed commits: %3$s.', ), ), '%s edited commit(s), added %s: %s; removed %s: %s.' => '%s edited commits, added %3$s; removed %5$s.', '%s added %s reverted commit(s): %s.' => array( array( '%s added a reverted commit: %3$s.', '%s added reverted commits: %3$s.', ), ), '%s removed %s reverted commit(s): %s.' => array( array( '%s removed a reverted commit: %3$s.', '%s removed reverted commits: %3$s.', ), ), '%s edited reverted commit(s), added %s: %s; removed %s: %s.' => '%s edited reverted commits, added %3$s; removed %5$s.', '%s added %s reverted commit(s) for %s: %s.' => array( array( '%s added a reverted commit for %3$s: %4$s.', '%s added reverted commits for %3$s: %4$s.', ), ), '%s removed %s reverted commit(s) for %s: %s.' => array( array( '%s removed a reverted commit for %3$s: %4$s.', '%s removed reverted commits for %3$s: %4$s.', ), ), '%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.' => '%s edited reverted commits for %2$s, added %4$s; removed %6$s.', '%s added %s reverting commit(s): %s.' => array( array( '%s added a reverting commit: %3$s.', '%s added reverting commits: %3$s.', ), ), '%s removed %s reverting commit(s): %s.' => array( array( '%s removed a reverting commit: %3$s.', '%s removed reverting commits: %3$s.', ), ), '%s edited reverting commit(s), added %s: %s; removed %s: %s.' => '%s edited reverting commits, added %3$s; removed %5$s.', '%s added %s reverting commit(s) for %s: %s.' => array( array( '%s added a reverting commit for %3$s: %4$s.', '%s added reverting commitsi for %3$s: %4$s.', ), ), '%s removed %s reverting commit(s) for %s: %s.' => array( array( '%s removed a reverting commit for %3$s: %4$s.', '%s removed reverting commits for %3$s: %4$s.', ), ), '%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.' => '%s edited reverting commits for %s, added %4$s; removed %6$s.', '%s changed project member(s), added %d: %s; removed %d: %s.' => '%s changed project members, added %3$s; removed %5$s.', '%s added %d project member(s): %s.' => array( array( '%s added a member: %3$s.', '%s added members: %3$s.', ), ), '%s removed %d project member(s): %s.' => array( array( '%s removed a member: %3$s.', '%s removed members: %3$s.', ), ), '%d project hashtag(s) are already used: %s.' => array( 'Project hashtag %2$s is already used.', '%d project hashtags are already used: %2$s.', ), '%s changed project hashtag(s), added %d: %s; removed %d: %s.' => '%s changed project hashtags, added %3$s; removed %5$s.', '%s added %d project hashtag(s): %s.' => array( array( '%s added a hashtag: %3$s.', '%s added hashtags: %3$s.', ), ), '%s removed %d project hashtag(s): %s.' => array( array( '%s removed a hashtag: %3$s.', '%s removed hashtags: %3$s.', ), ), '%s changed %s hashtag(s), added %d: %s; removed %d: %s.' => '%s changed hashtags for %s, added %4$s; removed %6$s.', '%s added %d %s hashtag(s): %s.' => array( array( '%s added a hashtag to %3$s: %4$s.', '%s added hashtags to %3$s: %4$s.', ), ), '%s removed %d %s hashtag(s): %s.' => array( array( '%s removed a hashtag from %3$s: %4$s.', '%s removed hashtags from %3$s: %4$s.', ), ), '%d User(s) Need Approval' => array( '%d User Needs Approval', '%d Users Need Approval', ), '%s older changes(s) are hidden.' => array( '%d older change is hidden.', '%d older changes are hidden.', ), '%s, %s line(s)' => array( '%s, %s line', '%s, %s lines', ), '%s pushed %d commit(s) to %s.' => array( array( '%s pushed a commit to %3$s.', '%s pushed %d commits to %s.', ), ), '%s commit(s)' => array( '1 commit', '%s commits', ), '%s removed %s JIRA issue(s): %s.' => array( array( '%s removed a JIRA issue: %3$s.', '%s removed JIRA issues: %3$s.', ), ), '%s added %s JIRA issue(s): %s.' => array( array( '%s added a JIRA issue: %3$s.', '%s added JIRA issues: %3$s.', ), ), '%s added %s required legal document(s): %s.' => array( array( '%s added a required legal document: %3$s.', '%s added required legal documents: %3$s.', ), ), '%s updated JIRA issue(s): added %s %s; removed %d %s.' => '%s updated JIRA issues: added %3$s; removed %5$s.', '%s edited %s task(s), added %s: %s; removed %s: %s.' => '%s edited tasks, added %4$s; removed %6$s.', '%s added %s task(s) to %s: %s.' => array( array( '%s added a task to %3$s: %4$s.', '%s added tasks to %3$s: %4$s.', ), ), '%s removed %s task(s) from %s: %s.' => array( array( '%s removed a task from %3$s: %4$s.', '%s removed tasks from %3$s: %4$s.', ), ), '%s edited %s task(s) for %s, added %s: %s; removed %s: %s.' => '%s edited tasks for %3$s, added: %5$s; removed %7$s.', '%s edited %s commit(s), added %s: %s; removed %s: %s.' => '%s edited commits, added %4$s; removed %6$s.', '%s added %s commit(s) to %s: %s.' => array( array( '%s added a commit to %3$s: %4$s.', '%s added commits to %3$s: %4$s.', ), ), '%s removed %s commit(s) from %s: %s.' => array( array( '%s removed a commit from %3$s: %4$s.', '%s removed commits from %3$s: %4$s.', ), ), '%s edited %s commit(s) for %s, added %s: %s; removed %s: %s.' => '%s edited commits for %3$s, added: %5$s; removed %7$s.', '%s added %s revision(s): %s.' => array( array( '%s added a revision: %3$s.', '%s added revisions: %3$s.', ), ), '%s removed %s revision(s): %s.' => array( array( '%s removed a revision: %3$s.', '%s removed revisions: %3$s.', ), ), '%s edited %s revision(s), added %s: %s; removed %s: %s.' => '%s edited revisions, added %4$s; removed %6$s.', '%s added %s revision(s) to %s: %s.' => array( array( '%s added a revision to %3$s: %4$s.', '%s added revisions to %3$s: %4$s.', ), ), '%s removed %s revision(s) from %s: %s.' => array( array( '%s removed a revision from %3$s: %4$s.', '%s removed revisions from %3$s: %4$s.', ), ), '%s edited %s revision(s) for %s, added %s: %s; removed %s: %s.' => '%s edited revisions for %3$s, added: %5$s; removed %7$s.', '%s edited %s project(s), added %s: %s; removed %s: %s.' => '%s edited projects, added %4$s; removed %6$s.', '%s added %s project(s) to %s: %s.' => array( array( '%s added a project to %3$s: %4$s.', '%s added projects to %3$s: %4$s.', ), ), '%s removed %s project(s) from %s: %s.' => array( array( '%s removed a project from %3$s: %4$s.', '%s removed projects from %3$s: %4$s.', ), ), '%s edited %s project(s) for %s, added %s: %s; removed %s: %s.' => '%s edited projects for %3$s, added: %5$s; removed %7$s.', '%s added %s panel(s): %s.' => array( array( '%s added a panel: %3$s.', '%s added panels: %3$s.', ), ), '%s removed %s panel(s): %s.' => array( array( '%s removed a panel: %3$s.', '%s removed panels: %3$s.', ), ), '%s edited %s panel(s), added %s: %s; removed %s: %s.' => '%s edited panels, added %4$s; removed %6$s.', '%s added %s dashboard(s): %s.' => array( array( '%s added a dashboard: %3$s.', '%s added dashboards: %3$s.', ), ), '%s removed %s dashboard(s): %s.' => array( array( '%s removed a dashboard: %3$s.', '%s removed dashboards: %3$s.', ), ), '%s edited %s dashboard(s), added %s: %s; removed %s: %s.' => '%s edited dashboards, added %4$s; removed %6$s.', '%s added %s edge(s): %s.' => array( array( '%s added an edge: %3$s.', '%s added edges: %3$s.', ), ), '%s added %s edge(s) to %s: %s.' => array( array( '%s added an edge to %3$s: %4$s.', '%s added edges to %3$s: %4$s.', ), ), '%s removed %s edge(s): %s.' => array( array( '%s removed an edge: %3$s.', '%s removed edges: %3$s.', ), ), '%s removed %s edge(s) from %s: %s.' => array( array( '%s removed an edge from %3$s: %4$s.', '%s removed edges from %3$s: %4$s.', ), ), '%s edited edge(s), added %s: %s; removed %s: %s.' => '%s edited edges, added: %3$s; removed: %5$s.', '%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.' => '%s edited edges for %3$s, added: %5$s; removed %7$s.', '%s added %s member(s) for %s: %s.' => array( array( '%s added a member for %3$s: %4$s.', '%s added members for %3$s: %4$s.', ), ), '%s removed %s member(s) for %s: %s.' => array( array( '%s removed a member for %3$s: %4$s.', '%s removed members for %3$s: %4$s.', ), ), '%s edited %s member(s) for %s, added %s: %s; removed %s: %s.' => '%s edited members for %3$s, added: %5$s; removed %7$s.', '%d related link(s):' => array( 'Related link:', 'Related links:', ), 'You have %d unpaid invoice(s).' => array( 'You have an unpaid invoice.', 'You have unpaid invoices.', ), 'The configurations differ in the following %s way(s):' => array( 'The configurations differ:', 'The configurations differ in these ways:', ), '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' => array( array( 'Phabricator is configured with an email domain whitelist (in %s), '. 'so only users with a verified email address at %3$s will be '. 'allowed to register an account.', 'Phabricator is configured with an email domain whitelist (in %s), '. 'so only users with a verified email address at one of these '. 'allowed domains will be able to register an account: %3$s', ), ), 'Show First %d Line(s)' => array( 'Show First Line', 'Show First %d Lines', ), "\xE2\x96\xB2 Show %d Line(s)" => array( "\xE2\x96\xB2 Show Line", "\xE2\x96\xB2 Show %d Lines", ), 'Show All %d Line(s)' => array( 'Show Line', 'Show All %d Lines', ), "\xE2\x96\xBC Show %d Line(s)" => array( "\xE2\x96\xBC Show Line", "\xE2\x96\xBC Show %d Lines", ), 'Show Last %d Line(s)' => array( 'Show Last Line', 'Show Last %d Lines', ), '%s marked %s inline comment(s) as done and %s inline comment(s) as '. 'not done.' => array( array( array( '%s marked an inline comment as done and an inline comment '. 'as not done.', '%s marked an inline comment as done and %3$s inline comments '. 'as not done.', ), array( '%s marked %s inline comments as done and an inline comment '. 'as not done.', '%s marked %s inline comments as done and %s inline comments '. 'as done.', ), ), ), '%s marked %s inline comment(s) as done.' => array( array( '%s marked an inline comment as done.', '%s marked %s inline comments as done.', ), ), '%s marked %s inline comment(s) as not done.' => array( array( '%s marked an inline comment as not done.', '%s marked %s inline comments as not done.', ), ), 'These %s object(s) will be destroyed forever:' => array( 'This object will be destroyed forever:', 'These objects will be destroyed forever:', ), 'Are you absolutely certain you want to destroy these %s '. 'object(s)?' => array( 'Are you absolutely certain you want to destroy this object?', 'Are you absolutely certain you want to destroy these objects?', ), '%s added %s owner(s): %s.' => array( array( '%s added an owner: %3$s.', '%s added owners: %3$s.', ), ), '%s removed %s owner(s): %s.' => array( array( '%s removed an owner: %3$s.', '%s removed owners: %3$s.', ), ), '%s changed %s package owner(s), added %s: %s; removed %s: %s.' => array( '%s changed package owners, added: %4$s; removed: %6$s.', ), 'Found %s book(s).' => array( 'Found %s book.', 'Found %s books.', ), 'Found %s file(s) in project.' => array( 'Found %s file in project.', 'Found %s files in project.', ), 'Found %s unatomized, uncached file(s).' => array( 'Found %s unatomized, uncached file.', 'Found %s unatomized, uncached files.', ), 'Found %s file(s) to atomize.' => array( 'Found %s file to atomize.', 'Found %s files to atomize.', ), 'Atomizing %s file(s).' => array( 'Atomizing %s file.', 'Atomizing %s files.', ), 'Creating %s document(s).' => array( 'Creating %s document.', 'Creating %s documents.', ), 'Deleting %s document(s).' => array( 'Deleting %s document.', 'Deleting %s documents.', ), 'Found %s obsolete atom(s) in graph.' => array( 'Found %s obsolete atom in graph.', 'Found %s obsolete atoms in graph.', ), 'Found %s new atom(s) in graph.' => array( 'Found %s new atom in graph.', 'Found %s new atoms in graph.', ), 'This call takes %s parameter(s), but only %s are documented.' => array( array( 'This call takes %s parameter, but only %s is documented.', 'This call takes %s parameter, but only %s are documented.', ), array( 'This call takes %s parameters, but only %s is documented.', 'This call takes %s parameters, but only %s are documented.', ), ), '%s Passed Test(s)' => '%s Passed', '%s Failed Test(s)' => '%s Failed', '%s Skipped Test(s)' => '%s Skipped', '%s Broken Test(s)' => '%s Broken', '%s Unsound Test(s)' => '%s Unsound', '%s Other Test(s)' => '%s Other', '%s Bulk Task(s)' => array( '%s Task', '%s Tasks', ), + '%s added %s badge(s) for %s: %s.' => array( array( '%s added a badge for %s: %3$s.', '%s added badges for %s: %3$s.', ), ), '%s added %s badge(s): %s.' => array( array( '%s added a badge: %3$s.', '%s added badges: %3$s.', ), ), '%s awarded %s recipient(s) for %s: %s.' => array( array( '%s awarded %3$s to %4$s.', '%s awarded %3$s to multiple recipients: %4$s.', ), ), '%s awarded %s recipients(s): %s.' => array( array( '%s awarded a recipient: %3$s.', '%s awarded multiple recipients: %3$s.', ), ), '%s edited badge(s) for %s, added %s: %s; revoked %s: %s.' => array( array( '%s edited badges for %s, added %s: %s; revoked %s: %s.', '%s edited badges for %s, added %s: %s; revoked %s: %s.', ), ), '%s edited badge(s), added %s: %s; revoked %s: %s.' => array( array( '%s edited badges, added %s: %s; revoked %s: %s.', '%s edited badges, added %s: %s; revoked %s: %s.', ), ), '%s edited recipient(s) for %s, awarded %s: %s; revoked %s: %s.' => array( array( '%s edited recipients for %s, awarded %s: %s; revoked %s: %s.', '%s edited recipients for %s, awarded %s: %s; revoked %s: %s.', ), ), '%s edited recipient(s), awarded %s: %s; revoked %s: %s.' => array( array( '%s edited recipients, awarded %s: %s; revoked %s: %s.', '%s edited recipients, awarded %s: %s; revoked %s: %s.', ), ), '%s revoked %s badge(s) for %s: %s.' => array( array( '%s revoked a badge for %3$s: %4$s.', '%s revoked multiple badges for %3$s: %4$s.', ), ), '%s revoked %s badge(s): %s.' => array( array( '%s revoked a badge: %3$s.', '%s revoked multiple badges: %3$s.', ), ), '%s revoked %s recipient(s) for %s: %s.' => array( array( '%s revoked %3$s from %4$s.', '%s revoked multiple recipients for %3$s: %4$s.', ), ), '%s revoked %s recipients(s): %s.' => array( array( '%s revoked a recipient: %3$s.', '%s revoked multiple recipients: %3$s.', ), ), + '%s automatically subscribed target(s) were not affected: %s.' => array( + 'An automatically subscribed target was not affected: %2$s.', + 'Automatically subscribed targets were not affected: %2$s.', + ), + + 'Declined to resubscribe %s target(s) because they previously '. + 'unsubscribed: %s.' => array( + 'Delined to resubscribe a target because they previously '. + 'unsubscribed: %2$s.', + 'Declined to resubscribe targets because they previously '. + 'unsubscribed: %2$s.', + ), + + '%s target(s) are not subscribed: %s.' => array( + 'A target is not subscribed: %2$s.', + 'Targets are not subscribed: %2$s.', + ), + + '%s target(s) are already subscribed: %s.' => array( + 'A target is already subscribed: %2$s.', + 'Targets are already subscribed: %2$s.', + ), + + 'Added %s subscriber(s): %s.' => array( + 'Added a subscriber: %2$s.', + 'Added subscribers: %2$s.', + ), + + 'Removed %s subscriber(s): %s.' => array( + 'Removed a subscriber: %2$s.', + 'Removed subscribers: %2$s.', + ), + + 'Queued email to be delivered to %s target(s): %s.' => array( + 'Queued email to be delivered to target: %2$s.', + 'Queued email to be delivered to targets: %2$s.', + ), + + 'Queued email to be delivered to %s target(s), ignoring their '. + 'notification preferences: %s.' => array( + 'Queued email to be delivered to target, ignoring notification '. + 'preferences: %2$s.', + 'Queued email to be delivered to targets, ignoring notification '. + 'preferences: %2$s.', + ), + + '%s project(s) are not associated: %s.' => array( + 'A project is not associated: %2$s.', + 'Projects are not associated: %2$s.', + ), + + '%s project(s) are already associated: %s.' => array( + 'A project is already associated: %2$s.', + 'Projects are already associated: %2$s.', + ), + + 'Added %s project(s): %s.' => array( + 'Added a project: %2$s.', + 'Added projects: %2$s.', + ), + + 'Removed %s project(s): %s.' => array( + 'Removed a project: %2$s.', + 'Removed projects: %2$s.', + ), + + 'Added %s reviewer(s): %s.' => array( + 'Added a reviewer: %2$s.', + 'Added reviewers: %2$s.', + ), + + 'Added %s blocking reviewer(s): %s.' => array( + 'Added a blocking reviewer: %2$s.', + 'Added blocking reviewers: %2$s.', + ), + + 'Required %s signature(s): %s.' => array( + 'Required a signature: %2$s.', + 'Required signatures: %2$s.', + ), + + 'Started %s build(s): %s.' => array( + 'Started a build: %2$s.', + 'Started builds: %2$s.', + ), + + 'Added %s auditor(s): %s.' => array( + 'Added an auditor: %2$s.', + 'Added auditors: %2$s.', + ), + + '%s target(s) do not have permission to see this object: %s.' => array( + 'A target does not have permission to see this object: %2$s.', + 'Targets do not have permission to see this object: %2$s.', + ), + + 'This action has no effect on %s target(s): %s.' => array( + 'This action has no effect on a target: %2$s.', + 'This action has no effect on targets: %2$s.', + ), + ); } } diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php index f5c936539e..896c52276f 100644 --- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php @@ -1,382 +1,382 @@ getObjectNamePrefix(); return preg_match('/^\w/', $prefix); } protected function getObjectIDPattern() { return '[1-9]\d*'; } protected function shouldMarkupObject(array $params) { return true; } protected function loadHandles(array $objects) { $phids = mpull($objects, 'getPHID'); $viewer = $this->getEngine()->getConfig('viewer'); $handles = $viewer->loadHandles($phids); $handles = iterator_to_array($handles); $result = array(); foreach ($objects as $id => $object) { $result[$id] = $handles[$object->getPHID()]; } return $result; } protected function getObjectHref( $object, PhabricatorObjectHandle $handle, $id) { $uri = $handle->getURI(); if ($this->getEngine()->getConfig('uri.full')) { $uri = PhabricatorEnv::getURI($uri); } return $uri; } - protected function renderObjectRefForAnyMedia ( + protected function renderObjectRefForAnyMedia( $object, PhabricatorObjectHandle $handle, $anchor, $id) { $href = $this->getObjectHref($object, $handle, $id); $text = $this->getObjectNamePrefix().$id; if ($anchor) { $href = $href.'#'.$anchor; $text = $text.'#'.$anchor; } if ($this->getEngine()->isTextMode()) { return PhabricatorEnv::getProductionURI($href); } else if ($this->getEngine()->isHTMLMailMode()) { $href = PhabricatorEnv::getProductionURI($href); return $this->renderObjectTagForMail($text, $href, $handle); } return $this->renderObjectRef($object, $handle, $anchor, $id); } protected function renderObjectRef( $object, PhabricatorObjectHandle $handle, $anchor, $id) { $href = $this->getObjectHref($object, $handle, $id); $text = $this->getObjectNamePrefix().$id; $status_closed = PhabricatorObjectHandle::STATUS_CLOSED; if ($anchor) { $href = $href.'#'.$anchor; $text = $text.'#'.$anchor; } $attr = array( 'phid' => $handle->getPHID(), 'closed' => ($handle->getStatus() == $status_closed), ); return $this->renderHovertag($text, $href, $attr); } protected function renderObjectEmbedForAnyMedia( $object, PhabricatorObjectHandle $handle, $options) { $name = $handle->getFullName(); $href = $handle->getURI(); if ($this->getEngine()->isTextMode()) { return $name.' <'.PhabricatorEnv::getProductionURI($href).'>'; } else if ($this->getEngine()->isHTMLMailMode()) { $href = PhabricatorEnv::getProductionURI($href); return $this->renderObjectTagForMail($name, $href, $handle); } return $this->renderObjectEmbed($object, $handle, $options); } protected function renderObjectEmbed( $object, PhabricatorObjectHandle $handle, $options) { $name = $handle->getFullName(); $href = $handle->getURI(); $status_closed = PhabricatorObjectHandle::STATUS_CLOSED; $attr = array( 'phid' => $handle->getPHID(), 'closed' => ($handle->getStatus() == $status_closed), ); return $this->renderHovertag($name, $href, $attr); } protected function renderObjectTagForMail( $text, $href, PhabricatorObjectHandle $handle) { $status_closed = PhabricatorObjectHandle::STATUS_CLOSED; $strikethrough = $handle->getStatus() == $status_closed ? 'text-decoration: line-through;' : 'text-decoration: none;'; return phutil_tag( 'a', array( 'href' => $href, 'style' => 'background-color: #e7e7e7; border-color: #e7e7e7; border-radius: 3px; padding: 0 4px; font-weight: bold; color: black;' .$strikethrough, ), $text); } protected function renderHovertag($name, $href, array $attr = array()) { return id(new PHUITagView()) ->setName($name) ->setHref($href) ->setType(PHUITagView::TYPE_OBJECT) ->setPHID(idx($attr, 'phid')) ->setClosed(idx($attr, 'closed')) ->render(); } public function apply($text) { $text = preg_replace_callback( $this->getObjectEmbedPattern(), array($this, 'markupObjectEmbed'), $text); $text = preg_replace_callback( $this->getObjectReferencePattern(), array($this, 'markupObjectReference'), $text); return $text; } private function getObjectEmbedPattern() { $prefix = $this->getObjectNamePrefix(); $prefix = preg_quote($prefix); $id = $this->getObjectIDPattern(); return '(\B{'.$prefix.'('.$id.')([,\s](?:[^}\\\\]|\\\\.)*)?}\B)u'; } private function getObjectReferencePattern() { $prefix = $this->getObjectNamePrefix(); $prefix = preg_quote($prefix); $id = $this->getObjectIDPattern(); // If the prefix starts with a word character (like "D"), we want to // require a word boundary so that we don't match "XD1" as "D1". If the // prefix does not start with a word character, we want to require no word // boundary for the same reasons. Test if the prefix starts with a word // character. if ($this->getObjectNamePrefixBeginsWithWordCharacter()) { $boundary = '\\b'; } else { $boundary = '\\B'; } // The "(?getObjectEmbedPattern(), $text, $embed_matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); $ref_matches = null; preg_match_all( $this->getObjectReferencePattern(), $text, $ref_matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); $results = array(); $sets = array( 'embed' => $embed_matches, 'ref' => $ref_matches, ); foreach ($sets as $type => $matches) { $formatted = array(); foreach ($matches as $match) { $format = array( 'offset' => $match[1][1], 'id' => $match[1][0], ); if (isset($match[2][0])) { $format['tail'] = $match[2][0]; } $formatted[] = $format; } $results[$type] = $formatted; } return $results; } public function markupObjectEmbed(array $matches) { if (!$this->isFlatText($matches[0])) { return $matches[0]; } return $this->markupObject(array( 'type' => 'embed', 'id' => $matches[1], 'options' => idx($matches, 2), 'original' => $matches[0], )); } public function markupObjectReference(array $matches) { if (!$this->isFlatText($matches[0])) { return $matches[0]; } return $this->markupObject(array( 'type' => 'ref', 'id' => $matches[1], 'anchor' => idx($matches, 2), 'original' => $matches[0], )); } private function markupObject(array $params) { if (!$this->shouldMarkupObject($params)) { return $params['original']; } $regex = trim( PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names')); if ($regex && preg_match($regex, $params['original'])) { return $params['original']; } $engine = $this->getEngine(); $token = $engine->storeText('x'); $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix(); $metadata = $engine->getTextMetadata($metadata_key, array()); $metadata[] = array( 'token' => $token, ) + $params; $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix(); $metadata = $engine->getTextMetadata($metadata_key, array()); if (!$metadata) { return; } $ids = ipull($metadata, 'id'); $objects = $this->loadObjects($ids); // For objects that are invalid or which the user can't see, just render // the original text. // TODO: We should probably distinguish between these cases and render a // "you can't see this" state for nonvisible objects. foreach ($metadata as $key => $spec) { if (empty($objects[$spec['id']])) { $engine->overwriteStoredText( $spec['token'], $spec['original']); unset($metadata[$key]); } } $phids = $engine->getTextMetadata(self::KEY_MENTIONED_OBJECTS, array()); foreach ($objects as $object) { $phids[$object->getPHID()] = $object->getPHID(); } $engine->setTextMetadata(self::KEY_MENTIONED_OBJECTS, $phids); $handles = $this->loadHandles($objects); foreach ($metadata as $key => $spec) { $handle = $handles[$spec['id']]; $object = $objects[$spec['id']]; switch ($spec['type']) { case 'ref': $view = $this->renderObjectRefForAnyMedia( $object, $handle, $spec['anchor'], $spec['id']); break; case 'embed': $spec['options'] = $this->assertFlatText($spec['options']); $view = $this->renderObjectEmbedForAnyMedia( $object, $handle, $spec['options']); break; } $engine->overwriteStoredText($spec['token'], $view); } $engine->setTextMetadata($metadata_key, array()); } } diff --git a/src/view/layout/AphrontTwoColumnView.php b/src/view/layout/AphrontTwoColumnView.php deleted file mode 100644 index c29a658ff5..0000000000 --- a/src/view/layout/AphrontTwoColumnView.php +++ /dev/null @@ -1,66 +0,0 @@ -mainColumn = $main; - return $this; - } - - public function setSideColumn($side) { - $this->sideColumn = $side; - return $this; - } - - public function setCentered($centered) { - $this->centered = $centered; - return $this; - } - - public function setNoPadding($padding) { - $this->padding = $padding; - return $this; - } - - public function render() { - require_celerity_resource('aphront-two-column-view-css'); - - $main = phutil_tag( - 'div', - array( - 'class' => 'aphront-main-column', - ), - $this->mainColumn); - - $side = phutil_tag( - 'div', - array( - 'class' => 'aphront-side-column', - ), - $this->sideColumn); - - $classes = array('aphront-two-column'); - if ($this->centered) { - $classes = array('aphront-two-column-centered'); - } - - if ($this->padding) { - $classes[] = 'aphront-two-column-padded'; - } - - return phutil_tag( - 'div', - array( - 'class' => implode(' ', $classes), - ), - array( - $main, - $side, - )); - } -} diff --git a/src/view/phui/PHUIBadgeMiniView.php b/src/view/phui/PHUIBadgeMiniView.php index 5807336ca2..92d6fd8b8b 100644 --- a/src/view/phui/PHUIBadgeMiniView.php +++ b/src/view/phui/PHUIBadgeMiniView.php @@ -1,63 +1,71 @@ icon = $icon; return $this; } public function setHref($href) { $this->href = $href; return $this; } public function setQuality($quality) { $this->quality = $quality; return $this; } public function setHeader($header) { $this->header = $header; return $this; } + public function setTipDirection($direction) { + $this->tipDirection = $direction; + return $this; + } + protected function getTagName() { if ($this->href) { return 'a'; } else { return 'span'; } } protected function getTagAttributes() { require_celerity_resource('phui-badge-view-css'); Javelin::initBehavior('phabricator-tooltips'); $classes = array(); $classes[] = 'phui-badge-mini'; if ($this->quality) { $classes[] = 'phui-badge-mini-'.$this->quality; } return array( 'class' => implode(' ', $classes), 'sigil' => 'has-tooltip', 'href' => $this->href, 'meta' => array( 'tip' => $this->header, + 'align' => $this->tipDirection, + 'size' => 300, ), ); } protected function getTagContent() { return id(new PHUIIconView()) ->setIconFont($this->icon); } } diff --git a/src/view/phui/PHUIIconView.php b/src/view/phui/PHUIIconView.php index 0cb7be8db5..08e27b0753 100644 --- a/src/view/phui/PHUIIconView.php +++ b/src/view/phui/PHUIIconView.php @@ -1,671 +1,750 @@ 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 setIconFont($icon, $color = null) { $this->iconFont = $icon; $this->iconColor = $color; return $this; } 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; } } 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); } return array( 'href' => $this->href, 'style' => $style, 'aural' => false, 'class' => $classes, ); } 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 getFontIcons() { 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-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', + ); } public static function getFontIconColors() { return array( 'bluegrey', 'white', 'red', 'orange', 'yellow', 'green', 'blue', 'sky', 'indigo', 'violet', 'pink', 'lightgreytext', 'lightbluetext', ); } } diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 626dcd2781..973592a94c 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -1,715 +1,741 @@ 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 getImageURI() { return $this->imageURI; } public function setImageIcon($image_icon) { $this->imageIcon = $image_icon; return $this; } public function getImageIcon() { return $this->imageIcon; } public function setState($state) { $this->state = $state; switch ($state) { case self::STATE_SUCCESS: $fi = 'fa-check-circle green'; break; case self::STATE_FAIL: $fi = 'fa-times-circle red'; break; case self::STATE_WARN: $fi = 'fa-exclamation-circle yellow'; break; case self::STATE_NOTE: $fi = 'fa-info-circle blue'; break; case self::STATE_BUILD: $fi = 'fa-refresh ph-spin sky'; break; } $this->setFontIcon($fi); return $this; } public function setFontIcon($icon) { $this->fontIcon = id(new PHUIIconView()) ->setIconFont($icon); return $this; } public function setEpoch($epoch, $age = self::AGE_FRESH) { $date = phabricator_datetime($epoch, $this->getUser()); $days = floor((time() - $epoch) / 60 / 60 / 24); switch ($age) { case self::AGE_FRESH: $this->addIcon('none', $date); break; case self::AGE_STALE: $attr = array( 'tip' => pht('Stale (%s day(s))', new PhutilNumber($days)), 'class' => 'icon-age-stale', ); $this->addIcon('fa-clock-o yellow', $date, $attr); break; case self::AGE_OLD: $attr = array( 'tip' => pht('Old (%s day(s))', new PhutilNumber($days)), 'class' => 'icon-age-old', ); $this->addIcon('fa-clock-o red', $date, $attr); break; default: throw new Exception(pht("Unknown age '%s'!", $age)); } 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; } 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; } protected function getTagName() { return 'li'; } protected function getTagAttributes() { $item_classes = array(); $item_classes[] = 'phui-object-item'; if ($this->icons) { $item_classes[] = 'phui-object-item-with-icons'; } if ($this->attributes) { $item_classes[] = 'phui-object-item-with-attrs'; } if ($this->handleIcons) { $item_classes[] = 'phui-object-item-with-handle-icons'; } if ($this->barColor) { $item_classes[] = 'phui-object-item-bar-color-'.$this->barColor; } else { $item_classes[] = 'phui-object-item-no-bar'; } if ($this->actions) { $n = count($this->actions); $item_classes[] = 'phui-object-item-with-actions'; $item_classes[] = 'phui-object-item-with-'.$n.'-actions'; } if ($this->disabled) { $item_classes[] = 'phui-object-item-disabled'; } if ($this->state) { $item_classes[] = 'phui-object-item-state-'.$this->state; } switch ($this->effect) { case 'highlighted': $item_classes[] = 'phui-object-item-highlighted'; break; case 'selected': $item_classes[] = 'phui-object-item-selected'; break; case null: break; default: throw new Exception(pht('Invalid effect!')); } if ($this->getGrippable()) { $item_classes[] = 'phui-object-item-grippable'; } if ($this->getImageURI()) { $item_classes[] = 'phui-object-item-with-image'; } if ($this->getImageIcon()) { $item_classes[] = 'phui-object-item-with-image-icon'; } if ($this->fontIcon) { $item_classes[] = 'phui-object-item-with-ficon'; } return array( 'class' => $item_classes, ); } protected function getTagContent() { $viewer = $this->getUser(); $content_classes = array(); $content_classes[] = 'phui-object-item-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-object-item-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-object-item-link', 'title' => $title_text, ), $this->header); $header = javelin_tag( 'div', array( 'class' => 'phui-object-item-name', 'sigil' => 'slippery', ), array( $this->headIcons, $header_name, $header_link, )); $icons = array(); if ($this->icons) { $icon_list = array(); foreach ($this->icons as $spec) { $icon = $spec['icon']; $icon = id(new PHUIIconView()) ->setIconFont($icon) ->addClass('phui-object-item-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-object-item-icon-label', ), $spec['label']); if (isset($spec['attributes']['href'])) { $icon_href = phutil_tag( 'a', array('href' => $spec['attributes']['href']), array($icon, $label)); } else { $icon_href = array($icon, $label); } $classes = array(); $classes[] = 'phui-object-item-icon'; if (isset($spec['attributes']['class'])) { $classes[] = $spec['attributes']['class']; } $icon_list[] = javelin_tag( 'li', array( 'class' => implode(' ', $classes), ), $icon_href); } $icons[] = phutil_tag( 'ul', array( 'class' => 'phui-object-item-icons', ), $icon_list); } if ($this->handleIcons) { $handle_bar = array(); foreach ($this->handleIcons as $handleicon) { $handle_bar[] = $this->renderHandleIcon($handleicon['icon'], $handleicon['label']); } $icons[] = phutil_tag( 'div', array( 'class' => 'phui-object-item-handle-icons', ), $handle_bar); } $bylines = array(); if ($this->bylines) { foreach ($this->bylines as $byline) { $bylines[] = phutil_tag( 'div', array( 'class' => 'phui-object-item-byline', ), $byline); } $bylines = phutil_tag( 'div', array( 'class' => 'phui-object-item-bylines', ), $bylines); } $subhead = null; if ($this->subhead) { $subhead = phutil_tag( 'div', array( 'class' => 'phui-object-item-subhead', ), $this->subhead); } if ($icons) { $icons = phutil_tag( 'div', array( 'class' => 'phui-object-icon-pane', ), $icons); } $attrs = null; if ($this->attributes) { $attrs = array(); $spacer = phutil_tag( 'span', array( 'class' => 'phui-object-item-attribute-spacer', ), "\xC2\xB7"); $first = true; foreach ($this->attributes as $attribute) { $attrs[] = phutil_tag( 'li', array( 'class' => 'phui-object-item-attribute', ), array( ($first ? null : $spacer), $attribute, )); $first = false; } $attrs = phutil_tag( 'ul', array( 'class' => 'phui-object-item-attributes', ), $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-object-item-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-object-item-image', 'style' => 'background-image: url('.$this->getImageURI().')', ), ''); } else if ($this->getImageIcon()) { $image = phutil_tag( 'div', array( 'class' => 'phui-object-item-image-icon', ), $this->getImageIcon()); } $ficon = null; if ($this->fontIcon) { $image = phutil_tag( 'div', array( 'class' => 'phui-object-item-ficon', ), $this->fontIcon); } if ($image && $this->href) { $image = phutil_tag( 'a', array( 'href' => $this->href, ), $image); } /* Build a fake table */ $column0 = null; if ($status) { $column0 = phutil_tag( 'div', array( 'class' => 'phui-object-item-col0', ), $status); } if ($this->badge) { $column0 = phutil_tag( 'div', array( 'class' => 'phui-object-item-col0 phui-object-item-badge', ), $this->badge); } + if ($this->countdownNum) { + $countdown = phutil_tag( + 'div', + array( + 'class' => 'phui-object-item-countdown-number', + ), + array( + phutil_tag_div('', $this->countdownNum), + phutil_tag_div('', $this->countdownNoun), + )); + $column0 = phutil_tag( + 'div', + array( + 'class' => 'phui-object-item-col0 phui-object-item-countdown', + ), + $countdown); + } + $column1 = phutil_tag( 'div', array( 'class' => 'phui-object-item-col1', ), array( $header, $content, )); $column2 = null; if ($icons || $bylines) { $column2 = phutil_tag( 'div', array( 'class' => 'phui-object-item-col2', ), array( $icons, $bylines, )); } $table = phutil_tag( 'div', array( 'class' => 'phui-object-item-table', ), phutil_tag_div( 'phui-object-item-table-row', array( $column0, $column1, $column2, ))); $box = phutil_tag( 'div', array( 'class' => 'phui-object-item-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-object-item-actions', ), $actions); } return phutil_tag( 'div', array( 'class' => 'phui-object-item-frame', ), array( $actions, $image, $box, )); } private function renderStatusIcon($icon, $label) { Javelin::initBehavior('phabricator-tooltips'); $icon = id(new PHUIIconView()) ->setIconFont($icon); $options = array( 'class' => 'phui-object-item-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-object-item-handle-icon', 'style' => 'background-image: url('.$handle->getImageURI().')', ); if (strlen($label)) { $options['sigil'] = 'has-tooltip'; $options['meta'] = array('tip' => $label); } return javelin_tag('span', $options, ''); } } diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php index 17178b8135..d8ba96c140 100644 --- a/src/view/phui/PHUITimelineView.php +++ b/src/view/phui/PHUITimelineView.php @@ -1,265 +1,266 @@ 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 setRenderData(array $data) { $this->renderData = $data; return $this; } 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, 'renderData' => $this->renderData, )); } $events = $this->buildEvents(); return phutil_tag( 'div', array( 'class' => 'phui-timeline-view', 'id' => $this->id, ), $events); } public function buildEvents() { require_celerity_resource('phui-timeline-view-css'); $spacer = self::renderSpacer(); $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 ($this->getPager()) { if ($this->getPager()->getHasMoreResults()) { $more = true; } } $events = array(); if ($more && $this->getPager()) { $uri = $this->getPager()->getNextPageURI(); $uri->setQueryParam('quoteTargetID', $this->getQuoteTargetID()); $uri->setQueryParam('quoteRef', $this->getQuoteRef()); $events[] = javelin_tag( 'div', array( 'sigil' => 'show-older-block', 'class' => 'phui-timeline-older-transactions-are-hidden', ), array( pht('Older changes are hidden. '), ' ', 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(true); } 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; $badge_edge_type = PhabricatorRecipientHasBadgeEdgeType::EDGECONST; $user_phids = array(); foreach ($events as $key => $event) { if (!$event->hasChildren()) { // This is a minor event, so we don't have space to show badges. unset($events[$key]); continue; } $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; } $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($user_phids) ->withEdgeTypes(array($badge_edge_type)); $edges->execute(); $badge_phids = $edges->getDestinationPHIDs(); if (!$badge_phids) { return; } $all_badges = id(new PhabricatorBadgesQuery()) ->setViewer($viewer) ->withPHIDs($badge_phids) ->execute(); $all_badges = mpull($all_badges, null, 'getPHID'); foreach ($events as $event) { $author_phid = $event->getAuthorPHID(); $event_phids = $edges->getDestinationPHIDs(array($author_phid)); $badges = array_select_keys($all_badges, $event_phids); // TODO: Pick the "best" badges in some smart way. For now, just pick // the first two. $badges = array_slice($badges, 0, 2); foreach ($badges as $badge) { $badge_view = id(new PHUIBadgeMiniView()) ->setIcon($badge->getIcon()) ->setQuality($badge->getQuality()) ->setHeader($badge->getName()) + ->setTipDirection('E') ->setHref('/badges/view/'.$badge->getID()); $event->addBadge($badge_view); } } } } diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index 06d15bbcc9..0308f542e5 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -1,598 +1,598 @@ browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function setImage($uri) { $this->image = $uri; return $this; } public function setInfoView(PHUIInfoView $error) { $this->error = $error; return $this; } public function __construct( $range_start, $range_end, $month, $year, $day = null) { $this->rangeStart = $range_start; $this->rangeEnd = $range_end; $this->day = $day; $this->month = $month; $this->year = $year; } public function render() { if (empty($this->user)) { throw new PhutilInvalidStateException('setUser'); } $events = msort($this->events, 'getEpochStart'); $days = $this->getDatesInMonth(); $cell_lists = array(); require_celerity_resource('phui-calendar-month-css'); foreach ($days as $day) { $day_number = $day->format('j'); $class = 'phui-calendar-month-day'; $weekday = $day->format('w'); $day->setTime(0, 0, 0); $day_start_epoch = $day->format('U'); $day_end_epoch = id(clone $day)->modify('+1 day')->format('U'); $list_events = array(); $all_day_events = array(); foreach ($events as $event) { if ($event->getEpochStart() >= $day_end_epoch) { break; } if ($event->getEpochStart() < $day_end_epoch && $event->getEpochEnd() > $day_start_epoch) { if ($event->getIsAllDay()) { $all_day_events[] = $event; } else { $list_events[] = $event; } } } $max_daily = 15; $counter = 0; $list = new PHUICalendarListView(); $list->setUser($this->user); foreach ($all_day_events as $item) { if ($counter <= $max_daily) { $list->addEvent($item); } $counter++; } foreach ($list_events as $item) { if ($counter <= $max_daily) { $list->addEvent($item); } $counter++; } $uri = $this->getBrowseURI(); $uri = $uri.$day->format('Y').'/'. $day->format('m').'/'. $day->format('d').'/'; $cell_lists[] = array( 'list' => $list, 'date' => $day, 'uri' => $uri, 'count' => count($all_day_events) + count($list_events), 'class' => $class, ); } $rows = array(); $cell_lists_by_week = array_chunk($cell_lists, 7); foreach ($cell_lists_by_week as $week_of_cell_lists) { $cells = array(); $max_count = $this->getMaxDailyEventsForWeek($week_of_cell_lists); foreach ($week_of_cell_lists as $cell_list) { $cells[] = $this->getEventListCell($cell_list, $max_count); } $rows[] = phutil_tag('tr', array(), $cells); $cells = array(); foreach ($week_of_cell_lists as $cell_list) { $cells[] = $this->getDayNumberCell($cell_list); } $rows[] = phutil_tag('tr', array(), $cells); } $header = $this->getDayNamesHeader(); $table = phutil_tag( 'table', array('class' => 'phui-calendar-view'), array( $header, $rows, )); $warnings = $this->getQueryRangeWarning(); $box = id(new PHUIObjectBoxView()) ->setHeader($this->renderCalendarHeader($this->getDateTime())) ->appendChild($table) ->setFormErrors($warnings); if ($this->error) { $box->setInfoView($this->error); } return $box; } private function getMaxDailyEventsForWeek($week_of_cell_lists) { $max_count = 0; foreach ($week_of_cell_lists as $cell_list) { if ($cell_list['count'] > $max_count) { $max_count = $cell_list['count']; } } return $max_count; } private function getEventListCell($event_list, $max_count = 0) { $list = $event_list['list']; $class = $event_list['class']; $uri = $event_list['uri']; $count = $event_list['count']; $viewer_is_invited = $list->getIsViewerInvitedOnList(); $event_count_badge = $this->getEventCountBadge($count, $viewer_is_invited); $cell_day_secret_link = $this->getHiddenDayLink($uri, $max_count, 125); $cell_data_div = phutil_tag( 'div', array( 'class' => 'phui-calendar-month-cell-div', ), array( $cell_day_secret_link, $event_count_badge, $list, )); return phutil_tag( 'td', array( 'class' => 'phui-calendar-month-event-list '.$class, ), $cell_data_div); } private function getDayNumberCell($event_list) { $class = $event_list['class']; $date = $event_list['date']; $cell_day_secret_link = null; $week_number = null; if ($date) { $uri = $event_list['uri']; $cell_day_secret_link = $this->getHiddenDayLink($uri, 0, 25); $cell_day = phutil_tag( 'a', array( 'class' => 'phui-calendar-date-number', 'href' => $uri, ), $date->format('j')); if ($date->format('w') == 1) { $week_number = phutil_tag( 'a', array( 'class' => 'phui-calendar-week-number', 'href' => $uri, ), $date->format('W')); } } else { $cell_day = null; } if ($date && $date->format('j') == $this->day && $date->format('m') == $this->month) { $today_class = 'phui-calendar-today-slot phui-calendar-today'; } else { $today_class = 'phui-calendar-today-slot'; } if ($this->isDateInCurrentWeek($date)) { $today_class .= ' phui-calendar-this-week'; } $last_week_day = 6; if ($date->format('w') == $last_week_day) { $today_class .= ' last-weekday'; } - $today_slot = phutil_tag ( + $today_slot = phutil_tag( 'div', array( 'class' => $today_class, ), null); $cell_div = phutil_tag( 'div', array( 'class' => 'phui-calendar-month-cell-div', ), array( $cell_day_secret_link, $week_number, $cell_day, $today_slot, )); return phutil_tag( 'td', array( 'class' => 'phui-calendar-date-number-container '.$class, ), $cell_div); } private function isDateInCurrentWeek($date) { list($week_start_date, $week_end_date) = $this->getThisWeekRange(); if ($date->format('U') < $week_end_date->format('U') && $date->format('U') >= $week_start_date->format('U')) { return true; } return false; } private function getEventCountBadge($count, $viewer_is_invited) { $class = 'phui-calendar-month-count-badge'; if ($viewer_is_invited) { $class = $class.' viewer-invited-day-badge'; } $event_count = null; if ($count > 0) { $event_count = phutil_tag( 'div', array( 'class' => $class, ), $count); } return phutil_tag( 'div', array( 'class' => 'phui-calendar-month-event-count', ), $event_count); } private function getHiddenDayLink($uri, $count, $max_height) { // approximately the height of the tallest cell $height = 18 * $count + 5; $height = ($height > $max_height) ? $height : $max_height; $height_style = 'height: '.$height.'px'; return phutil_tag( 'a', array( 'class' => 'phui-calendar-month-secret-link', 'style' => $height_style, 'href' => $uri, ), null); } private function getDayNamesHeader() { list($week_start, $week_end) = $this->getWeekStartAndEnd(); $weekday_names = array( $this->getDayHeader(pht('Sun'), pht('Sunday'), true), $this->getDayHeader(pht('Mon'), pht('Monday')), $this->getDayHeader(pht('Tue'), pht('Tuesday')), $this->getDayHeader(pht('Wed'), pht('Wednesday')), $this->getDayHeader(pht('Thu'), pht('Thursday')), $this->getDayHeader(pht('Fri'), pht('Friday')), $this->getDayHeader(pht('Sat'), pht('Saturday'), true), ); $sorted_weekday_names = array(); for ($i = $week_start; $i < ($week_start + 7); $i++) { $sorted_weekday_names[] = $weekday_names[$i % 7]; } return phutil_tag( 'tr', array('class' => 'phui-calendar-day-of-week-header'), $sorted_weekday_names); } private function getDayHeader($short, $long, $is_weekend = false) { $class = null; if ($is_weekend) { $class = 'weekend-day-header'; } $day = array(); $day[] = phutil_tag( 'span', array( 'class' => 'long-weekday-name', ), $long); $day[] = phutil_tag( 'span', array( 'class' => 'short-weekday-name', ), $short); return phutil_tag( 'th', array( 'class' => $class, ), $day); } private function renderCalendarHeader(DateTime $date) { $button_bar = null; // check for a browseURI, which means we need "fancy" prev / next UI $uri = $this->getBrowseURI(); if ($uri) { list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); $prev_uri = $uri.$prev_year.'/'.$prev_month.'/'; list($next_year, $next_month) = $this->getNextYearAndMonth(); $next_uri = $uri.$next_year.'/'.$next_month.'/'; $button_bar = new PHUIButtonBarView(); $left_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-left bluegrey'); $left = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($prev_uri) ->setTitle(pht('Previous Month')) ->setIcon($left_icon); $right_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-right bluegrey'); $right = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($next_uri) ->setTitle(pht('Next Month')) ->setIcon($right_icon); $button_bar->addButton($left); $button_bar->addButton($right); } $header = id(new PHUIHeaderView()) ->setHeader($date->format('F Y')); if ($button_bar) { $header->setButtonBar($button_bar); } if ($this->image) { $header->setImage($this->image); } return $header; } private function getQueryRangeWarning() { $errors = array(); $range_start_epoch = null; $range_end_epoch = null; if ($this->rangeStart) { $range_start_epoch = $this->rangeStart->getEpoch(); } if ($this->rangeEnd) { $range_end_epoch = $this->rangeEnd->getEpoch(); } $month_start = $this->getDateTime(); $month_end = id(clone $month_start)->modify('+1 month'); $month_start = $month_start->format('U'); $month_end = $month_end->format('U') - 1; if (($range_start_epoch != null && $range_start_epoch < $month_end && $range_start_epoch > $month_start) || ($range_end_epoch != null && $range_end_epoch < $month_end && $range_end_epoch > $month_start)) { $errors[] = pht('Part of the month is out of range'); } if (($range_end_epoch != null && $range_end_epoch < $month_start) || ($range_start_epoch != null && $range_start_epoch > $month_end)) { $errors[] = pht('Month is out of query range'); } return $errors; } private function getNextYearAndMonth() { $next = $this->getDateTime(); $next->modify('+1 month'); return array( $next->format('Y'), $next->format('m'), ); } private function getPrevYearAndMonth() { $prev = $this->getDateTime(); $prev->modify('-1 month'); return array( $prev->format('Y'), $prev->format('m'), ); } /** * Return a DateTime object representing the first moment in each day in the * month, according to the user's locale. * * @return list List of DateTimes, one for each day. */ private function getDatesInMonth() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; list($next_year, $next_month) = $this->getNextYearAndMonth(); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); list($start_of_week, $end_of_week) = $this->getWeekStartAndEnd(); $days_in_month = id(clone $end_date)->modify('-1 day')->format('d'); $first_month_day_date = new DateTime("{$year}-{$month}-01", $timezone); $last_month_day_date = id(clone $end_date)->modify('-1 day'); $first_weekday_of_month = $first_month_day_date->format('w'); $last_weekday_of_month = $last_month_day_date->format('w'); $day_date = id(clone $first_month_day_date); $num_days_display = $days_in_month; if ($start_of_week !== $first_weekday_of_month) { $interim_start_num = ($first_weekday_of_month + 7 - $start_of_week) % 7; $num_days_display += $interim_start_num; $day_date->modify('-'.$interim_start_num.' days'); } if ($end_of_week !== $last_weekday_of_month) { $interim_end_day_num = ($end_of_week - $last_weekday_of_month + 7) % 7; $num_days_display += $interim_end_day_num; $end_date->modify('+'.$interim_end_day_num.' days'); } $days = array(); for ($day = 1; $day <= $num_days_display; $day++) { $day_epoch = $day_date->format('U'); $end_epoch = $end_date->format('U'); if ($day_epoch >= $end_epoch) { break; } else { $days[] = clone $day_date; } $day_date->modify('+1 day'); } return $days; } private function getTodayMidnight() { $viewer = $this->getUser(); $today = new DateTime('@'.time()); $today->setTimeZone($viewer->getTimeZone()); $today->setTime(0, 0, 0); return $today; } private function getThisWeekRange() { list($week_start, $week_end) = $this->getWeekStartAndEnd(); $today = $this->getTodayMidnight(); $date_weekday = $today->format('w'); $days_from_week_start = ($date_weekday + 7 - $week_start) % 7; $days_to_week_end = 7 - $days_from_week_start; $modify = '-'.$days_from_week_start.' days'; $week_start_date = id(clone $today)->modify($modify); $modify = '+'.$days_to_week_end.' days'; $week_end_date = id(clone $today)->modify($modify); return array($week_start_date, $week_end_date); } private function getWeekStartAndEnd() { $preferences = $this->user->loadPreferences(); $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $week_start = $preferences->getPreference($pref_week_start, 0); $week_end = ($week_start + 6) % 7; return array($week_start, $week_end); } private function getDateTime() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; $date = new DateTime("{$year}-{$month}-01 ", $timezone); return $date; } } diff --git a/webroot/rsrc/css/aphront/two-column.css b/webroot/rsrc/css/aphront/two-column.css deleted file mode 100644 index e9a0217e8c..0000000000 --- a/webroot/rsrc/css/aphront/two-column.css +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @provides aphront-two-column-view-css - */ - -.aphront-two-column { - position: relative; -} - -.device-desktop .aphront-two-column.aphront-two-column-padded { - margin: 20px; -} - -.device-desktop .aphront-two-column .aphront-main-column { - margin-right: 300px; -} - -.device-desktop .aphront-two-column .aphront-side-column { - width: 300px; - position: absolute; - top: 0; - right: 0; -} - -.device-desktop .aphront-two-column-centered { - width: 980px; - margin: 0 auto; -} - -.device-desktop .aphront-two-column-centered .aphront-main-column { - float: left; - width: 820px; -} - -.device-desktop .aphront-two-column-centered .aphront-side-column { - width: 160px; - float: right; -} - -.device-phone .aphront-two-column.aphront-two-column-padded { - margin: 10px; -} diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index 9da3de6d7f..886700ccf5 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -1,346 +1,350 @@ /** * @provides conpherence-message-pane-css */ .conpherence-message-pane, .loading .messages-loading-mask, .loading .messages-loading-icon, .conpherence-layout .conpherence-no-threads { position: fixed; left: 241px; right: 241px; top: 76px; bottom: 0px; min-width: 300px; width: auto; } .device .conpherence-message-pane, .device .loading .messages-loading-mask, .device .loading .messages-loading-icon, .device .conpherence-layout .conpherence-no-threads { left: 0; right: 0; width: 100%; } .conpherence-layout .conpherence-no-threads { text-align: center; } .conpherence-layout .conpherence-no-threads .text { margin: 16px 0px 16px 0px; } .conpherence-layout .phui-crumbs-view { padding: 0 0 0 8px; background: #f7f7f7; } .conpherence-show-more-messages { display: block; background: #e0e3ec; margin: 10px; text-align: center; padding: 10px; color: {$bluetext}; } .conpherence-show-more-messages-loading { font-style: italic; } .conpherence-message-pane .conpherence-messages { position: fixed; left: 241px; right: 241px; top: 76px; bottom: 172px; overflow-x: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; } .page-has-warning .conpherence-message-pane .conpherence-messages { top: 110px; } .conpherence-messages.jx-scrollbar-frame { overflow-y: hidden; } .conpherence-messages .jx-scrollbar-content > .conpherence-edited:first-child, .conpherence-messages > .conpherence-edited:first-child { padding-top: 20px; } .conpherence-messages > .jx-scrollbar-content .conpherence-edited:last-child, .conpherence-messages > .conpherence-edited:last-child { padding-bottom: 20px; } .conpherence-message-pane .conpherence-edited + .date-marker { margin-top: 24px; } .device .conpherence-message-pane .conpherence-messages { left: 0; right: 0; bottom: 52px; width: 100%; box-shadow: none; } .conpherence-message-pane .messages-loading-mask { opacity: .6; background: #fff; display: none; } .loading .messages-loading-mask { display: block; } .conpherence-message-pane .phui-form-view { border-width: 0; background-color: {$lightgreybackground}; height: 156px; padding: 8px; position: fixed; bottom: 0; border-top: 1px solid {$thinblueborder}; left: 241px; right: 241px; } .conpherence-message-pane .phui-form-view.login-to-participate { height: 28px; } .conpherence-message-pane .login-to-participate a.button { float: right; } .conpherence-message-pane .aphront-form-control-submit button, .conpherence-message-pane .aphront-form-control-submit a.button { margin-top: 6px; } /** * When entering "Fullscreen Mode" in the remarkup control, we need to drop * all of the "position: fixed" on parent elements or Chrome doesn't put the * textarea on top. */ .remarkup-fullscreen-mode .conpherence-message-pane, .remarkup-fullscreen-mode .conpherence-message-pane .conpherence-messages, .remarkup-fullscreen-mode .conpherence-message-pane .phui-form-view, .remarkup-fullscreen-mode .conpherence-layout { position: static; } .conpherence-message-pane .remarkup-assist-bar { border-color: {$lightblueborder}; } .device .conpherence-message-pane .remarkup-assist-bar { display: none; } .device .conpherence-message-pane .phui-form-view { left: 0; right: 0; height: 34px; width: auto; } .conpherence-layout .conpherence-message-pane .phui-form-view div.aphront-form-input { margin: 0; width: 100%; } .conpherence-message-pane .conpherence-transaction-view { padding: 2px 0px; margin: 4px 12px; background-size: 100%; min-height: auto; } .device-phone .conpherence-message-pane .conpherence-transaction-view { margin: 0 8px; } .conpherence-message-pane .conpherence-transaction-image { float: left; border-radius: 3px; height: 35px; width: 35px; background-size: 35px; position: absolute; top: 5px; box-shadow: {$borderinset}; } .device-phone .conpherence-message-pane .conpherence-transaction-image { display: none; } .conpherence-message-pane .conpherence-comment.anchor-target, .conpherence-message-pane .conpherence-edited.anchor-target { background: {$lightyellow}; } .conpherence-message-pane .conpherence-comment.anchor-target { margin: 4px 8px 4px 8px; padding: 2px 4px 2px 4px; } .conpherence-message-pane .conpherence-edited.anchor-target { margin: 0px 8px 0px 8px; padding: 0px 4px 0px 4px; } .conpherence-message-pane .conpherence-transaction-detail { border-width: 0; margin-left: 45px; } .device-phone .conpherence-message-pane .conpherence-transaction-detail { margin: 0; } .conpherence-message-pane .conpherence-transaction-view.date-marker { padding: 0; margin: 20px 12px 4px; min-height: auto; } .device-phone .conpherence-message-pane .conpherence-transaction-view.date-marker { margin: 12px 0 4px; } .device-tablet .conpherence-message-pane .conpherence-transaction-view.date-marker { padding-left: 37px; } .conpherence-message-pane .conpherence-transaction-view.date-marker .date { left: 40px; font-size: {$normalfontsize}; padding: 0px 4px; } .device .conpherence-message-pane .conpherence-transaction-view.date-marker .date { color: {$lightbluetext}; left: 4px; } .device-phone .conpherence-message-pane .conpherence-edited { min-height: none; color: {$lightgreytext}; margin: 0 8px; } .conpherence-message-pane .conpherence-edited .conpherence-transaction-content { color: {$lightgreytext}; font-size: {$normalfontsize}; margin: 0; padding: 0; float: left; } .conpherence-message-pane .conpherence-edited { padding: 0; margin-top: 0; margin-bottom: 0; min-height: inherit; } .conpherence-message-pane .conpherence-edited + .conpherence-comment { margin-top: 16px; } .conpherence-transaction-view.conpherence-edited + .conpherence-transaction-view.date-marker { margin-top: 24px; } .conpherence-message-pane .conpherence-edited .conpherence-transaction-header { float: right; } .conpherence-message-pane .conpherence-edited .conpherence-transaction-content a { color: {$darkbluetext}; } .device-phone .conpherence-message-pane .conpherence-transaction-info { display: none; } .conpherence-message-pane .conpherence-transaction-info, .conpherence-message-pane .anchor-link, .conpherence-message-pane .phabricator-content-source-view { color: {$lightgreytext}; line-height: 16px; font-size: {$smallerfontsize}; } .conpherence-message-pane .conpherence-transaction-info { float: right; } .conpherence-message-pane .conpherence-transaction-header, .conpherence-message-pane .conpherence-transaction-info, .conpherence-message-pane .anchor-link, .conpherence-message-pane .conpherence-transaction-content { background: none; padding: 0; } .conpherence-message-pane .conpherence-transaction-content { padding: 2px 0 8px 0; } .conpherence-message-pane .aphront-form-control { padding: 0; } .conpherence-message-pane .remarkup-assist-textarea { height: 100px; padding: 6px; border-color: {$lightblueborder}; border-top-color: {$thinblueborder}; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; resize: none; } .device .conpherence-message-pane .remarkup-assist-textarea { margin: 0; padding: 8px 68px 8px 8px; width: 100%; height: 34px; resize: none; } .conpherence-message-pane .remarkup-assist-textarea:focus { outline: none; } .device .conpherence-message-pane .aphront-form-control-submit { padding: 0; position: absolute; top: 7px; right: 13px; } .device .conpherence-message-pane .aphront-form-control-textarea { float: left; height: 24px; width: 100%; } + +.conpherence-message .phabricator-remarkup .remarkup-code-block pre { + max-height: 200px; +} diff --git a/webroot/rsrc/css/application/countdown/timer.css b/webroot/rsrc/css/application/countdown/timer.css index 34a732128c..8b87aa6ed2 100644 --- a/webroot/rsrc/css/application/countdown/timer.css +++ b/webroot/rsrc/css/application/countdown/timer.css @@ -1,50 +1,76 @@ /** * @provides phabricator-countdown-css */ .phabricator-timer { - margin: 16px; + margin: 16px 16px 0 16px; border: 1px solid {$lightblueborder}; - border-radius: 4px; + border-radius: 3px; background: #ffffff; } +.device .phabricator-timer { + margin-left: 8px; + margin-right: 8px; +} + .phabricator-remarkup .phabricator-timer { - width: 360px; + max-width: 360px; margin: 0 0 12px 0; } +.device-phone .phabricator-remarkup .phabricator-timer { + width: auto; +} + .phabricator-timer-header { - font-size: {$biggerfontsize}; font-weight: bold; - padding: 4px 8px; + padding: 8px; background: {$bluebackground}; border-radius: 3px 3px 0 0; - border-bottom: 1px solid {$lightblueborder}; +} + +.phabricator-timer-header a { + color: {$darkbluetext}; + font-weight: normal; } .phabricator-timer-table { width: 100%; - margin: 8px 0; + margin: 8px 0 0 0; } .phabricator-timer-table th { text-align: center; - color: {$greytext}; + font-size: {$biggerfontsize}; + color: {$bluetext}; width: 10%; padding: 4px; } .phabricator-timer-table td { padding: 4px; text-align: center; font-size: 36px; + font-weight: bold; overflow: hidden; line-height: auto; height: auto; - line-height: 36px; + line-height: 44px; +} + +.phabricator-timer-table td.phabricator-timer-foot { + font-size: {$normalfontsize}; + line-height: 16px; + border-top: 1px solid {$thinblueborder}; + color: {$bluetext}; + font-weight: normal; + padding: 8px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + text-align: center; } .phabricator-timer-table small { color: {$lightgreytext}; } diff --git a/webroot/rsrc/css/application/herald/herald-test.css b/webroot/rsrc/css/application/herald/herald-test.css index 178d54653d..67b49145d5 100644 --- a/webroot/rsrc/css/application/herald/herald-test.css +++ b/webroot/rsrc/css/application/herald/herald-test.css @@ -1,85 +1,16 @@ /** * @provides herald-test-css */ -ul.herald-explain-list { - margin: 12px; -} - -div.herald-condition-note { - clear: both; - margin: .5em 0em .53em 86px; - padding: .5em 1em; - background: #FFFF00; - font-weight: bold; -} - -ul.herald-explain-list .herald-outcome { - margin-right: 6px; - width: 50px; - text-align: center; - position: relative; - float: left; - font-weight: bold; - padding: 1px 2px; -} - -ul.herald-explain-list .condition-fail, -ul.herald-explain-list .rule-fail { +.herald-condition-note { color: {$red}; } -ul.herald-explain-list .condition-pass, -ul.herald-explain-list .rule-pass { - color: {$green}; -} - -ul.herald-explain-list li.herald-rule-pass, -ul.herald-explain-list li.herald-rule-fail { - margin: 0 0 8px; -} - -ul.herald-explain-list div.rule-name { - padding: 4px 0 8px 0; -} - -ul.herald-explain-list li.herald-rule-fail, -ul.herald-explain-list li.herald-rule-pass { - background: #ffffff; -} - -ul.herald-explain-list ul { - margin: 4px; -} - -ul.herald-explain-list ul li { - padding: 2px 0; -} - -.outcome-failure, -.outcome-success { - font-weight: bold; -} - -.outcome-failure { - color: {$red}; -} - -.outcome-success { - color: {$green}; -} - -.action-header { - font-weight: bold; - padding-top: 12px; - border-bottom: 1px solid {$thinblueborder}; -} - textarea.herald-field-value-transcript { width: 100%; height: 10em; } .condition-test-value { color: {$greytext}; } diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index 2569711095..095602b22b 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -1,123 +1,9 @@ /** * @provides phame-css */ -.notice { - background: #F3F3FF; - border: 1px solid #008; - margin: 16px 16px 4px 26px; -} -.notice h3 { - background: #E3E3FF; - padding: 8px; -} - -.notice h4 { - font-weight: normal; - padding: 8px; -} - -.phame-post-preview-header { - margin: 0px 0px 16px 0px; -} - -.device-desktop .phame-post-list { - max-width: 600px; -} - -.phame-post-list .phui-info-view { - margin: 0; -} - -.phame-post-list .phui-icon-view:hover { - text-decoration: none; -} - -.blog-post-list { - clear: left; - float: left; - width: 70%; - margin: 16px 0px 16px 16px; - padding: 0px 8px 12px 8px; -} - -.device .blog-post-list { - float: none; - width: 90%; - margin: 16px auto; -} - -.blog-post-list-full { - clear: left; - float: left; - margin: 16px 0px 0px 0px; - padding: 0px 16px 0px 16px; -} - -.device .blog-post-list-full { - float: none; - margin: 16px auto; -} - -.blog-detail { - float: right; - clear: right; - width: 20%; - margin: 16px 16px 16px 0px; -} - -.device .blog-detail { - float: none; - margin: 16px auto; - width: 90%; -} - -.blog-detail .description { - margin: 16px 0px 16px 0px; -} - -.blog-detail .bloggers { - font-size: {$smallerfontsize}; -} - -.blog-post, -.blog-detail { - border: 1px solid #DBDBDB; - background: #F9F9F9; - padding: 20px; -} - -.blog-post { - margin: 0px 0px 20px 0px; -} - -.blog-post .header { - padding: 0px 0px 16px 0px; -} - -.blog-post .header h1 { - clear: none; -} - -.blog-post .header .last-updated { - color: {$greytext}; - clear: none; - font-size: {$smallerfontsize}; -} - -.blog-post .header .buttons { - float: right; -} -.blog-post .header .buttons a { - margin: 0px 0px 0px 12px; -} - -.more-and-comments { - padding: 12px 0px 12px 0px; -} - .fb-comments, .fb-comments span, .fb-comments iframe[style] { width: 100% !important; } diff --git a/webroot/rsrc/css/application/ponder/comments.css b/webroot/rsrc/css/application/ponder/comments.css deleted file mode 100644 index ca712b1d74..0000000000 --- a/webroot/rsrc/css/application/ponder/comments.css +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @provides ponder-comment-table-css - */ - -.ponder-show-comments { - text-align: center; - padding: 8px; - margin: 0 16px; - float: right; - font-weight: bold; - background: #fff; - border-bottom: 1px solid {$blueborder}; - border-left: 1px solid {$lightblueborder}; - border-right: 1px solid {$lightblueborder}; -} - -.ponder-comments { - width: 480px; - margin: 5px 0 0 60px; -} - -.device .ponder-comments { - width: 100%; - margin: 5px 0 0 0; -} - -.ponder-comments th { - width: 0px; - height: 0px; -} - -.ponder-comments td { - vertical-align: top; - padding: 6px; - border-bottom: 1px solid {$thinblueborder}; - background: #fff; -} - -.ponder-datestamp { - font-size: {$smallestfontsize}; - color: {$greytext}; -} - -.ponder-label { - display: block; - width: 100%; - font-weight: bold; - color: #333; - text-align: left; - margin: 0px 0px 6px; - padding: 6px 4px; - background: #ccc; - border-bottom: 1px solid #aaa; - cursor: pointer; -} - -td .aphront-form-control { - padding: 0; -} - -td .aphront-form-control-submit { - display: block; -} - -td .aphront-form-input { - margin: 0; - width: 100%; -} - -td .aphront-form-control textarea { - height: 50px; -} - -.ponder-comment-markup p { - margin: 0 0 5px 0; -} - -.ponder-comment-markup h2, -.ponder-comment-markup h3, -.ponder-comment-markup h4, -.ponder-comment-markup h5 { - margin: 0; - font-size: inherit; - font-weight: normal; -} diff --git a/webroot/rsrc/css/application/ponder/feed.css b/webroot/rsrc/css/application/ponder/feed.css deleted file mode 100644 index e2c0eefb21..0000000000 --- a/webroot/rsrc/css/application/ponder/feed.css +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @provides ponder-feed-view-css - */ - -.ponder-question-summary { - width: 100%; - background: #DFDFE3; - float: left; - clear: both; - margin-top: 1px; - padding: 1px; -} - -.ponder-answer-summary { - width : 100%; - background : #DFDFE3; - float : left; - clear : both; - margin-top : 1px; - padding : 1px; -} - -.ponder-summary-votes { - width : 50px; - height : 36pt; - font-size : 18pt; - text-align : center; - background : #EEE; - border : 1px solid #BBB; - float : left; - margin : 2px; - padding-top : 2px; -} - -.ponder-summary-answers { - width : 50px; - height : 36pt; - font-size : 18pt; - text-align : center; - background : #EEE; - border : 1px solid #BBB; - float : left; - margin : 2px; - padding-top : 2px; -} - -.ponder-question-label { - font-size : 6pt; -} - -h2.ponder-question-title { - font-size : 12pt; - margin : 2px; - padding : 0; -} - -h2.ponder-answer-title { - font-size : 12pt; - margin : 2px; - padding : 0; -} - -.ponder-metadata { - padding-left : 5px; - width : 650px; - float : left; -} - -.ponder-small-metadata { - font-size : 7.5pt; - color : #555; - margin : 0; - text-align : right; -} diff --git a/webroot/rsrc/css/application/ponder/vote.css b/webroot/rsrc/css/application/ponder/ponder-view.css similarity index 61% rename from webroot/rsrc/css/application/ponder/vote.css rename to webroot/rsrc/css/application/ponder/ponder-view.css index 3436df9882..237c3e7676 100644 --- a/webroot/rsrc/css/application/ponder/vote.css +++ b/webroot/rsrc/css/application/ponder/ponder-view.css @@ -1,42 +1,59 @@ /** - * @provides ponder-vote-css + * @provides ponder-view-css */ .ponder-votable { float: right; margin: 4px 0 4px 24px; } .ponder-votebox { border-radius: 4px; background: #f3f3f3; border: 1px solid {$blueborder}; text-align: center; width: 24px; } .ponder-votebox a { font-size: 20px; line-height: 24px; display: block; text-decoration: none; color: #aaaaaa; font-weight: normal; } .ponder-votebox a.ponder-vote-active { color: {$blue}; } .ponder-votebox a:hover { color: #ffffff; background: {$blue}; } .ponder-vote-count { color: {$darkbluetext}; font-size: {$biggerfontsize}; line-height: 20px; font-weight: bold; } + +.ponder-show-comments { + text-align: center; + padding: 8px; + margin: 0 16px; + float: right; + font-weight: bold; + background: #fff; + border-bottom: 1px solid {$blueborder}; + border-left: 1px solid {$lightblueborder}; + border-right: 1px solid {$lightblueborder}; +} + +.device-desktop .ponder-comments-view { + width: 90%; + margin: 0 auto; +} diff --git a/webroot/rsrc/css/application/ponder/post.css b/webroot/rsrc/css/application/ponder/post.css deleted file mode 100644 index c1f3c67c55..0000000000 --- a/webroot/rsrc/css/application/ponder/post.css +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @provides ponder-post-css - */ - -.ponder-post-list { - max-width: 1162px; -} - -.ponder-add-answer-panel { - max-width: 1162px; -} - -.ponder-post-list .anchor-target { - background-color: #ffffdd; - border-color: #ffff00; -} - -.ponder-post-core .phabricator-remarkup .remarkup-code-block { - width: 88ex; - width: 81ch; -} - -.ponder-question { - background: white; -} diff --git a/webroot/rsrc/css/font/font-awesome.css b/webroot/rsrc/css/font/font-awesome.css index 7766362550..1658708f4e 100644 --- a/webroot/rsrc/css/font/font-awesome.css +++ b/webroot/rsrc/css/font/font-awesome.css @@ -1,1675 +1,1889 @@ /** * @provides font-fontawesome */ /*! - * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.4.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ /* FONT PATH * -------------------------- */ @font-face { font-family: 'FontAwesome'; - src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?v=4.3.0'); - src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf?v=4.3.0') format('truetype'); + src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?v=4.4.0'); + src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?#iefix&v=4.4.0') format('embedded-opentype'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2?v=4.4.0') format('woff2'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff?v=4.4.0') format('woff'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf?v=4.4.0') format('truetype'); font-weight: normal; font-style: normal; } .phui-font-fa:before { font-family: FontAwesome; } /* makes the font 33% larger relative to the icon container */ .fa-lg { font-size: 1.33333333em; line-height: 0.75em; vertical-align: -15%; } .fa-2x { font-size: 2em; } .fa-3x { font-size: 3em; } .fa-4x { font-size: 4em; } .fa-5x { font-size: 5em; } .fa-fw { width: 1.28571429em; text-align: center; } + /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */ .fa-glass:before { content: "\f000"; } .fa-music:before { content: "\f001"; } .fa-search:before { content: "\f002"; } .fa-envelope-o:before { content: "\f003"; } .fa-heart:before { content: "\f004"; } .fa-star:before { content: "\f005"; } .fa-star-o:before { content: "\f006"; } .fa-user:before { content: "\f007"; } .fa-film:before { content: "\f008"; } .fa-th-large:before { content: "\f009"; } .fa-th:before { content: "\f00a"; } .fa-th-list:before { content: "\f00b"; } .fa-check:before { content: "\f00c"; } .fa-remove:before, .fa-close:before, .fa-times:before { content: "\f00d"; } .fa-search-plus:before { content: "\f00e"; } .fa-search-minus:before { content: "\f010"; } .fa-power-off:before { content: "\f011"; } .fa-signal:before { content: "\f012"; } .fa-gear:before, .fa-cog:before { content: "\f013"; } .fa-trash-o:before { content: "\f014"; } .fa-home:before { content: "\f015"; } .fa-file-o:before { content: "\f016"; } .fa-clock-o:before { content: "\f017"; } .fa-road:before { content: "\f018"; } .fa-download:before { content: "\f019"; } .fa-arrow-circle-o-down:before { content: "\f01a"; } .fa-arrow-circle-o-up:before { content: "\f01b"; } .fa-inbox:before { content: "\f01c"; } .fa-play-circle-o:before { content: "\f01d"; } .fa-rotate-right:before, .fa-repeat:before { content: "\f01e"; } .fa-refresh:before { content: "\f021"; } .fa-list-alt:before { content: "\f022"; } .fa-lock:before { content: "\f023"; } .fa-flag:before { content: "\f024"; } .fa-headphones:before { content: "\f025"; } .fa-volume-off:before { content: "\f026"; } .fa-volume-down:before { content: "\f027"; } .fa-volume-up:before { content: "\f028"; } .fa-qrcode:before { content: "\f029"; } .fa-barcode:before { content: "\f02a"; } .fa-tag:before { content: "\f02b"; } .fa-tags:before { content: "\f02c"; } .fa-book:before { content: "\f02d"; } .fa-bookmark:before { content: "\f02e"; } .fa-print:before { content: "\f02f"; } .fa-camera:before { content: "\f030"; } .fa-font:before { content: "\f031"; } .fa-bold:before { content: "\f032"; } .fa-italic:before { content: "\f033"; } .fa-text-height:before { content: "\f034"; } .fa-text-width:before { content: "\f035"; } .fa-align-left:before { content: "\f036"; } .fa-align-center:before { content: "\f037"; } .fa-align-right:before { content: "\f038"; } .fa-align-justify:before { content: "\f039"; } .fa-list:before { content: "\f03a"; } .fa-dedent:before, .fa-outdent:before { content: "\f03b"; } .fa-indent:before { content: "\f03c"; } .fa-video-camera:before { content: "\f03d"; } .fa-photo:before, .fa-image:before, .fa-picture-o:before { content: "\f03e"; } .fa-pencil:before { content: "\f040"; } .fa-map-marker:before { content: "\f041"; } .fa-adjust:before { content: "\f042"; } .fa-tint:before { content: "\f043"; } .fa-edit:before, .fa-pencil-square-o:before { content: "\f044"; } .fa-share-square-o:before { content: "\f045"; } .fa-check-square-o:before { content: "\f046"; } .fa-arrows:before { content: "\f047"; } .fa-step-backward:before { content: "\f048"; } .fa-fast-backward:before { content: "\f049"; } .fa-backward:before { content: "\f04a"; } .fa-play:before { content: "\f04b"; } .fa-pause:before { content: "\f04c"; } .fa-stop:before { content: "\f04d"; } .fa-forward:before { content: "\f04e"; } .fa-fast-forward:before { content: "\f050"; } .fa-step-forward:before { content: "\f051"; } .fa-eject:before { content: "\f052"; } .fa-chevron-left:before { content: "\f053"; } .fa-chevron-right:before { content: "\f054"; } .fa-plus-circle:before { content: "\f055"; } .fa-minus-circle:before { content: "\f056"; } .fa-times-circle:before { content: "\f057"; } .fa-check-circle:before { content: "\f058"; } .fa-question-circle:before { content: "\f059"; } .fa-info-circle:before { content: "\f05a"; } .fa-crosshairs:before { content: "\f05b"; } .fa-times-circle-o:before { content: "\f05c"; } .fa-check-circle-o:before { content: "\f05d"; } .fa-ban:before { content: "\f05e"; } .fa-arrow-left:before { content: "\f060"; } .fa-arrow-right:before { content: "\f061"; } .fa-arrow-up:before { content: "\f062"; } .fa-arrow-down:before { content: "\f063"; } .fa-mail-forward:before, .fa-share:before { content: "\f064"; } .fa-expand:before { content: "\f065"; } .fa-compress:before { content: "\f066"; } .fa-plus:before { content: "\f067"; } .fa-minus:before { content: "\f068"; } .fa-asterisk:before { content: "\f069"; } .fa-exclamation-circle:before { content: "\f06a"; } .fa-gift:before { content: "\f06b"; } .fa-leaf:before { content: "\f06c"; } .fa-fire:before { content: "\f06d"; } .fa-eye:before { content: "\f06e"; } .fa-eye-slash:before { content: "\f070"; } .fa-warning:before, .fa-exclamation-triangle:before { content: "\f071"; } .fa-plane:before { content: "\f072"; } .fa-calendar:before { content: "\f073"; } .fa-random:before { content: "\f074"; } .fa-comment:before { content: "\f075"; } .fa-magnet:before { content: "\f076"; } .fa-chevron-up:before { content: "\f077"; } .fa-chevron-down:before { content: "\f078"; } .fa-retweet:before { content: "\f079"; } .fa-shopping-cart:before { content: "\f07a"; } .fa-folder:before { content: "\f07b"; } .fa-folder-open:before { content: "\f07c"; } .fa-arrows-v:before { content: "\f07d"; } .fa-arrows-h:before { content: "\f07e"; } .fa-bar-chart-o:before, .fa-bar-chart:before { content: "\f080"; } .fa-twitter-square:before { content: "\f081"; } .fa-facebook-square:before { content: "\f082"; } .fa-camera-retro:before { content: "\f083"; } .fa-key:before { content: "\f084"; } .fa-gears:before, .fa-cogs:before { content: "\f085"; } .fa-comments:before { content: "\f086"; } .fa-thumbs-o-up:before { content: "\f087"; } .fa-thumbs-o-down:before { content: "\f088"; } .fa-star-half:before { content: "\f089"; } .fa-heart-o:before { content: "\f08a"; } .fa-sign-out:before { content: "\f08b"; } .fa-linkedin-square:before { content: "\f08c"; } .fa-thumb-tack:before { content: "\f08d"; } .fa-external-link:before { content: "\f08e"; } .fa-sign-in:before { content: "\f090"; } .fa-trophy:before { content: "\f091"; } .fa-github-square:before { content: "\f092"; } .fa-upload:before { content: "\f093"; } .fa-lemon-o:before { content: "\f094"; } .fa-phone:before { content: "\f095"; } .fa-square-o:before { content: "\f096"; } .fa-bookmark-o:before { content: "\f097"; } .fa-phone-square:before { content: "\f098"; } .fa-twitter:before { content: "\f099"; } .fa-facebook-f:before, .fa-facebook:before { content: "\f09a"; } .fa-github:before { content: "\f09b"; } .fa-unlock:before { content: "\f09c"; } .fa-credit-card:before { content: "\f09d"; } +.fa-feed:before, .fa-rss:before { content: "\f09e"; } .fa-hdd-o:before { content: "\f0a0"; } .fa-bullhorn:before { content: "\f0a1"; } .fa-bell:before { content: "\f0f3"; } .fa-certificate:before { content: "\f0a3"; } .fa-hand-o-right:before { content: "\f0a4"; } .fa-hand-o-left:before { content: "\f0a5"; } .fa-hand-o-up:before { content: "\f0a6"; } .fa-hand-o-down:before { content: "\f0a7"; } .fa-arrow-circle-left:before { content: "\f0a8"; } .fa-arrow-circle-right:before { content: "\f0a9"; } .fa-arrow-circle-up:before { content: "\f0aa"; } .fa-arrow-circle-down:before { content: "\f0ab"; } .fa-globe:before { content: "\f0ac"; } .fa-wrench:before { content: "\f0ad"; } .fa-tasks:before { content: "\f0ae"; } .fa-filter:before { content: "\f0b0"; } .fa-briefcase:before { content: "\f0b1"; } .fa-arrows-alt:before { content: "\f0b2"; } .fa-group:before, .fa-users:before { content: "\f0c0"; } .fa-chain:before, .fa-link:before { content: "\f0c1"; } .fa-cloud:before { content: "\f0c2"; } .fa-flask:before { content: "\f0c3"; } .fa-cut:before, .fa-scissors:before { content: "\f0c4"; } .fa-copy:before, .fa-files-o:before { content: "\f0c5"; } .fa-paperclip:before { content: "\f0c6"; } .fa-save:before, .fa-floppy-o:before { content: "\f0c7"; } .fa-square:before { content: "\f0c8"; } .fa-navicon:before, .fa-reorder:before, .fa-bars:before { content: "\f0c9"; } .fa-list-ul:before { content: "\f0ca"; } .fa-list-ol:before { content: "\f0cb"; } .fa-strikethrough:before { content: "\f0cc"; } .fa-underline:before { content: "\f0cd"; } .fa-table:before { content: "\f0ce"; } .fa-magic:before { content: "\f0d0"; } .fa-truck:before { content: "\f0d1"; } .fa-pinterest:before { content: "\f0d2"; } .fa-pinterest-square:before { content: "\f0d3"; } .fa-google-plus-square:before { content: "\f0d4"; } .fa-google-plus:before { content: "\f0d5"; } .fa-money:before { content: "\f0d6"; } .fa-caret-down:before { content: "\f0d7"; } .fa-caret-up:before { content: "\f0d8"; } .fa-caret-left:before { content: "\f0d9"; } .fa-caret-right:before { content: "\f0da"; } .fa-columns:before { content: "\f0db"; } .fa-unsorted:before, .fa-sort:before { content: "\f0dc"; } .fa-sort-down:before, .fa-sort-desc:before { content: "\f0dd"; } .fa-sort-up:before, .fa-sort-asc:before { content: "\f0de"; } .fa-envelope:before { content: "\f0e0"; } .fa-linkedin:before { content: "\f0e1"; } .fa-rotate-left:before, .fa-undo:before { content: "\f0e2"; } .fa-legal:before, .fa-gavel:before { content: "\f0e3"; } .fa-dashboard:before, .fa-tachometer:before { content: "\f0e4"; } .fa-comment-o:before { content: "\f0e5"; } .fa-comments-o:before { content: "\f0e6"; } .fa-flash:before, .fa-bolt:before { content: "\f0e7"; } .fa-sitemap:before { content: "\f0e8"; } .fa-umbrella:before { content: "\f0e9"; } .fa-paste:before, .fa-clipboard:before { content: "\f0ea"; } .fa-lightbulb-o:before { content: "\f0eb"; } .fa-exchange:before { content: "\f0ec"; } .fa-cloud-download:before { content: "\f0ed"; } .fa-cloud-upload:before { content: "\f0ee"; } .fa-user-md:before { content: "\f0f0"; } .fa-stethoscope:before { content: "\f0f1"; } .fa-suitcase:before { content: "\f0f2"; } .fa-bell-o:before { content: "\f0a2"; } .fa-coffee:before { content: "\f0f4"; } .fa-cutlery:before { content: "\f0f5"; } .fa-file-text-o:before { content: "\f0f6"; } .fa-building-o:before { content: "\f0f7"; } .fa-hospital-o:before { content: "\f0f8"; } .fa-ambulance:before { content: "\f0f9"; } .fa-medkit:before { content: "\f0fa"; } .fa-fighter-jet:before { content: "\f0fb"; } .fa-beer:before { content: "\f0fc"; } .fa-h-square:before { content: "\f0fd"; } .fa-plus-square:before { content: "\f0fe"; } .fa-angle-double-left:before { content: "\f100"; } .fa-angle-double-right:before { content: "\f101"; } .fa-angle-double-up:before { content: "\f102"; } .fa-angle-double-down:before { content: "\f103"; } .fa-angle-left:before { content: "\f104"; } .fa-angle-right:before { content: "\f105"; } .fa-angle-up:before { content: "\f106"; } .fa-angle-down:before { content: "\f107"; } .fa-desktop:before { content: "\f108"; } .fa-laptop:before { content: "\f109"; } .fa-tablet:before { content: "\f10a"; } .fa-mobile-phone:before, .fa-mobile:before { content: "\f10b"; } .fa-circle-o:before { content: "\f10c"; } .fa-quote-left:before { content: "\f10d"; } .fa-quote-right:before { content: "\f10e"; } .fa-spinner:before { content: "\f110"; } .fa-circle:before { content: "\f111"; } .fa-mail-reply:before, .fa-reply:before { content: "\f112"; } .fa-github-alt:before { content: "\f113"; } .fa-folder-o:before { content: "\f114"; } .fa-folder-open-o:before { content: "\f115"; } .fa-smile-o:before { content: "\f118"; } .fa-frown-o:before { content: "\f119"; } .fa-meh-o:before { content: "\f11a"; } .fa-gamepad:before { content: "\f11b"; } .fa-keyboard-o:before { content: "\f11c"; } .fa-flag-o:before { content: "\f11d"; } .fa-flag-checkered:before { content: "\f11e"; } .fa-terminal:before { content: "\f120"; } .fa-code:before { content: "\f121"; } .fa-mail-reply-all:before, .fa-reply-all:before { content: "\f122"; } .fa-star-half-empty:before, .fa-star-half-full:before, .fa-star-half-o:before { content: "\f123"; } .fa-location-arrow:before { content: "\f124"; } .fa-crop:before { content: "\f125"; } .fa-code-fork:before { content: "\f126"; } .fa-unlink:before, .fa-chain-broken:before { content: "\f127"; } .fa-question:before { content: "\f128"; } .fa-info:before { content: "\f129"; } .fa-exclamation:before { content: "\f12a"; } .fa-superscript:before { content: "\f12b"; } .fa-subscript:before { content: "\f12c"; } .fa-eraser:before { content: "\f12d"; } .fa-puzzle-piece:before { content: "\f12e"; } .fa-microphone:before { content: "\f130"; } .fa-microphone-slash:before { content: "\f131"; } .fa-shield:before { content: "\f132"; } .fa-calendar-o:before { content: "\f133"; } .fa-fire-extinguisher:before { content: "\f134"; } .fa-rocket:before { content: "\f135"; } .fa-maxcdn:before { content: "\f136"; } .fa-chevron-circle-left:before { content: "\f137"; } .fa-chevron-circle-right:before { content: "\f138"; } .fa-chevron-circle-up:before { content: "\f139"; } .fa-chevron-circle-down:before { content: "\f13a"; } .fa-html5:before { content: "\f13b"; } .fa-css3:before { content: "\f13c"; } .fa-anchor:before { content: "\f13d"; } .fa-unlock-alt:before { content: "\f13e"; } .fa-bullseye:before { content: "\f140"; } .fa-ellipsis-h:before { content: "\f141"; } .fa-ellipsis-v:before { content: "\f142"; } .fa-rss-square:before { content: "\f143"; } .fa-play-circle:before { content: "\f144"; } .fa-ticket:before { content: "\f145"; } .fa-minus-square:before { content: "\f146"; } .fa-minus-square-o:before { content: "\f147"; } .fa-level-up:before { content: "\f148"; } .fa-level-down:before { content: "\f149"; } .fa-check-square:before { content: "\f14a"; } .fa-pencil-square:before { content: "\f14b"; } .fa-external-link-square:before { content: "\f14c"; } .fa-share-square:before { content: "\f14d"; } .fa-compass:before { content: "\f14e"; } .fa-toggle-down:before, .fa-caret-square-o-down:before { content: "\f150"; } .fa-toggle-up:before, .fa-caret-square-o-up:before { content: "\f151"; } .fa-toggle-right:before, .fa-caret-square-o-right:before { content: "\f152"; } .fa-euro:before, .fa-eur:before { content: "\f153"; } .fa-gbp:before { content: "\f154"; } .fa-dollar:before, .fa-usd:before { content: "\f155"; } .fa-rupee:before, .fa-inr:before { content: "\f156"; } .fa-cny:before, .fa-rmb:before, .fa-yen:before, .fa-jpy:before { content: "\f157"; } .fa-ruble:before, .fa-rouble:before, .fa-rub:before { content: "\f158"; } .fa-won:before, .fa-krw:before { content: "\f159"; } .fa-bitcoin:before, .fa-btc:before { content: "\f15a"; } .fa-file:before { content: "\f15b"; } .fa-file-text:before { content: "\f15c"; } .fa-sort-alpha-asc:before { content: "\f15d"; } .fa-sort-alpha-desc:before { content: "\f15e"; } .fa-sort-amount-asc:before { content: "\f160"; } .fa-sort-amount-desc:before { content: "\f161"; } .fa-sort-numeric-asc:before { content: "\f162"; } .fa-sort-numeric-desc:before { content: "\f163"; } .fa-thumbs-up:before { content: "\f164"; } .fa-thumbs-down:before { content: "\f165"; } .fa-youtube-square:before { content: "\f166"; } .fa-youtube:before { content: "\f167"; } .fa-xing:before { content: "\f168"; } .fa-xing-square:before { content: "\f169"; } .fa-youtube-play:before { content: "\f16a"; } .fa-dropbox:before { content: "\f16b"; } .fa-stack-overflow:before { content: "\f16c"; } .fa-instagram:before { content: "\f16d"; } .fa-flickr:before { content: "\f16e"; } .fa-adn:before { content: "\f170"; } .fa-bitbucket:before { content: "\f171"; } .fa-bitbucket-square:before { content: "\f172"; } .fa-tumblr:before { content: "\f173"; } .fa-tumblr-square:before { content: "\f174"; } .fa-long-arrow-down:before { content: "\f175"; } .fa-long-arrow-up:before { content: "\f176"; } .fa-long-arrow-left:before { content: "\f177"; } .fa-long-arrow-right:before { content: "\f178"; } .fa-apple:before { content: "\f179"; } .fa-windows:before { content: "\f17a"; } .fa-android:before { content: "\f17b"; } .fa-linux:before { content: "\f17c"; } .fa-dribbble:before { content: "\f17d"; } .fa-skype:before { content: "\f17e"; } .fa-foursquare:before { content: "\f180"; } .fa-trello:before { content: "\f181"; } .fa-female:before { content: "\f182"; } .fa-male:before { content: "\f183"; } .fa-gittip:before, .fa-gratipay:before { content: "\f184"; } .fa-sun-o:before { content: "\f185"; } .fa-moon-o:before { content: "\f186"; } .fa-archive:before { content: "\f187"; } .fa-bug:before { content: "\f188"; } .fa-vk:before { content: "\f189"; } .fa-weibo:before { content: "\f18a"; } .fa-renren:before { content: "\f18b"; } .fa-pagelines:before { content: "\f18c"; } .fa-stack-exchange:before { content: "\f18d"; } .fa-arrow-circle-o-right:before { content: "\f18e"; } .fa-arrow-circle-o-left:before { content: "\f190"; } .fa-toggle-left:before, .fa-caret-square-o-left:before { content: "\f191"; } .fa-dot-circle-o:before { content: "\f192"; } .fa-wheelchair:before { content: "\f193"; } .fa-vimeo-square:before { content: "\f194"; } .fa-turkish-lira:before, .fa-try:before { content: "\f195"; } .fa-plus-square-o:before { content: "\f196"; } .fa-space-shuttle:before { content: "\f197"; } .fa-slack:before { content: "\f198"; } .fa-envelope-square:before { content: "\f199"; } .fa-wordpress:before { content: "\f19a"; } .fa-openid:before { content: "\f19b"; } .fa-institution:before, .fa-bank:before, .fa-university:before { content: "\f19c"; } .fa-mortar-board:before, .fa-graduation-cap:before { content: "\f19d"; } .fa-yahoo:before { content: "\f19e"; } .fa-google:before { content: "\f1a0"; } .fa-reddit:before { content: "\f1a1"; } .fa-reddit-square:before { content: "\f1a2"; } .fa-stumbleupon-circle:before { content: "\f1a3"; } .fa-stumbleupon:before { content: "\f1a4"; } .fa-delicious:before { content: "\f1a5"; } .fa-digg:before { content: "\f1a6"; } .fa-pied-piper:before { content: "\f1a7"; } .fa-pied-piper-alt:before { content: "\f1a8"; } .fa-drupal:before { content: "\f1a9"; } .fa-joomla:before { content: "\f1aa"; } .fa-language:before { content: "\f1ab"; } .fa-fax:before { content: "\f1ac"; } .fa-building:before { content: "\f1ad"; } .fa-child:before { content: "\f1ae"; } .fa-paw:before { content: "\f1b0"; } .fa-spoon:before { content: "\f1b1"; } .fa-cube:before { content: "\f1b2"; } .fa-cubes:before { content: "\f1b3"; } .fa-behance:before { content: "\f1b4"; } .fa-behance-square:before { content: "\f1b5"; } .fa-steam:before { content: "\f1b6"; } .fa-steam-square:before { content: "\f1b7"; } .fa-recycle:before { content: "\f1b8"; } .fa-automobile:before, .fa-car:before { content: "\f1b9"; } .fa-cab:before, .fa-taxi:before { content: "\f1ba"; } .fa-tree:before { content: "\f1bb"; } .fa-spotify:before { content: "\f1bc"; } .fa-deviantart:before { content: "\f1bd"; } .fa-soundcloud:before { content: "\f1be"; } .fa-database:before { content: "\f1c0"; } .fa-file-pdf-o:before { content: "\f1c1"; } .fa-file-word-o:before { content: "\f1c2"; } .fa-file-excel-o:before { content: "\f1c3"; } .fa-file-powerpoint-o:before { content: "\f1c4"; } .fa-file-photo-o:before, .fa-file-picture-o:before, .fa-file-image-o:before { content: "\f1c5"; } .fa-file-zip-o:before, .fa-file-archive-o:before { content: "\f1c6"; } .fa-file-sound-o:before, .fa-file-audio-o:before { content: "\f1c7"; } .fa-file-movie-o:before, .fa-file-video-o:before { content: "\f1c8"; } .fa-file-code-o:before { content: "\f1c9"; } .fa-vine:before { content: "\f1ca"; } .fa-codepen:before { content: "\f1cb"; } .fa-jsfiddle:before { content: "\f1cc"; } .fa-life-bouy:before, .fa-life-buoy:before, .fa-life-saver:before, .fa-support:before, .fa-life-ring:before { content: "\f1cd"; } .fa-circle-o-notch:before { content: "\f1ce"; } .fa-ra:before, .fa-rebel:before { content: "\f1d0"; } .fa-ge:before, .fa-empire:before { content: "\f1d1"; } .fa-git-square:before { content: "\f1d2"; } .fa-git:before { content: "\f1d3"; } +.fa-y-combinator-square:before, +.fa-yc-square:before, .fa-hacker-news:before { content: "\f1d4"; } .fa-tencent-weibo:before { content: "\f1d5"; } .fa-qq:before { content: "\f1d6"; } .fa-wechat:before, .fa-weixin:before { content: "\f1d7"; } .fa-send:before, .fa-paper-plane:before { content: "\f1d8"; } .fa-send-o:before, .fa-paper-plane-o:before { content: "\f1d9"; } .fa-history:before { content: "\f1da"; } -.fa-genderless:before, .fa-circle-thin:before { content: "\f1db"; } .fa-header:before { content: "\f1dc"; } .fa-paragraph:before { content: "\f1dd"; } .fa-sliders:before { content: "\f1de"; } .fa-share-alt:before { content: "\f1e0"; } .fa-share-alt-square:before { content: "\f1e1"; } .fa-bomb:before { content: "\f1e2"; } .fa-soccer-ball-o:before, .fa-futbol-o:before { content: "\f1e3"; } .fa-tty:before { content: "\f1e4"; } .fa-binoculars:before { content: "\f1e5"; } .fa-plug:before { content: "\f1e6"; } .fa-slideshare:before { content: "\f1e7"; } .fa-twitch:before { content: "\f1e8"; } .fa-yelp:before { content: "\f1e9"; } .fa-newspaper-o:before { content: "\f1ea"; } .fa-wifi:before { content: "\f1eb"; } .fa-calculator:before { content: "\f1ec"; } .fa-paypal:before { content: "\f1ed"; } .fa-google-wallet:before { content: "\f1ee"; } .fa-cc-visa:before { content: "\f1f0"; } .fa-cc-mastercard:before { content: "\f1f1"; } .fa-cc-discover:before { content: "\f1f2"; } .fa-cc-amex:before { content: "\f1f3"; } .fa-cc-paypal:before { content: "\f1f4"; } .fa-cc-stripe:before { content: "\f1f5"; } .fa-bell-slash:before { content: "\f1f6"; } .fa-bell-slash-o:before { content: "\f1f7"; } .fa-trash:before { content: "\f1f8"; } .fa-copyright:before { content: "\f1f9"; } .fa-at:before { content: "\f1fa"; } .fa-eyedropper:before { content: "\f1fb"; } .fa-paint-brush:before { content: "\f1fc"; } .fa-birthday-cake:before { content: "\f1fd"; } .fa-area-chart:before { content: "\f1fe"; } .fa-pie-chart:before { content: "\f200"; } .fa-line-chart:before { content: "\f201"; } .fa-lastfm:before { content: "\f202"; } .fa-lastfm-square:before { content: "\f203"; } .fa-toggle-off:before { content: "\f204"; } .fa-toggle-on:before { content: "\f205"; } .fa-bicycle:before { content: "\f206"; } .fa-bus:before { content: "\f207"; } .fa-ioxhost:before { content: "\f208"; } .fa-angellist:before { content: "\f209"; } .fa-cc:before { content: "\f20a"; } .fa-shekel:before, .fa-sheqel:before, .fa-ils:before { content: "\f20b"; } .fa-meanpath:before { content: "\f20c"; } .fa-buysellads:before { content: "\f20d"; } .fa-connectdevelop:before { content: "\f20e"; } .fa-dashcube:before { content: "\f210"; } .fa-forumbee:before { content: "\f211"; } .fa-leanpub:before { content: "\f212"; } .fa-sellsy:before { content: "\f213"; } .fa-shirtsinbulk:before { content: "\f214"; } .fa-simplybuilt:before { content: "\f215"; } .fa-skyatlas:before { content: "\f216"; } .fa-cart-plus:before { content: "\f217"; } .fa-cart-arrow-down:before { content: "\f218"; } .fa-diamond:before { content: "\f219"; } .fa-ship:before { content: "\f21a"; } .fa-user-secret:before { content: "\f21b"; } .fa-motorcycle:before { content: "\f21c"; } .fa-street-view:before { content: "\f21d"; } .fa-heartbeat:before { content: "\f21e"; } .fa-venus:before { content: "\f221"; } .fa-mars:before { content: "\f222"; } .fa-mercury:before { content: "\f223"; } +.fa-intersex:before, .fa-transgender:before { content: "\f224"; } .fa-transgender-alt:before { content: "\f225"; } .fa-venus-double:before { content: "\f226"; } .fa-mars-double:before { content: "\f227"; } .fa-venus-mars:before { content: "\f228"; } .fa-mars-stroke:before { content: "\f229"; } .fa-mars-stroke-v:before { content: "\f22a"; } .fa-mars-stroke-h:before { content: "\f22b"; } .fa-neuter:before { content: "\f22c"; } +.fa-genderless:before { + content: "\f22d"; +} .fa-facebook-official:before { content: "\f230"; } .fa-pinterest-p:before { content: "\f231"; } .fa-whatsapp:before { content: "\f232"; } .fa-server:before { content: "\f233"; } .fa-user-plus:before { content: "\f234"; } .fa-user-times:before { content: "\f235"; } .fa-hotel:before, .fa-bed:before { content: "\f236"; } .fa-viacoin:before { content: "\f237"; } .fa-train:before { content: "\f238"; } .fa-subway:before { content: "\f239"; } .fa-medium:before { content: "\f23a"; } +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} diff --git a/webroot/rsrc/css/phui/phui-badge.css b/webroot/rsrc/css/phui/phui-badge.css index e5f04e2707..f45a88f258 100644 --- a/webroot/rsrc/css/phui/phui-badge.css +++ b/webroot/rsrc/css/phui/phui-badge.css @@ -1,218 +1,221 @@ /** * @provides phui-badge-view-css */ .phui-badge-flex-view { padding: 12px 4px 8px; overflow: hidden; text-align: center; } .phui-badge-flex-item { display: inline-block; padding: 4px 8px; } .phui-badge-flex-view.flex-view-collapsed { padding: 0; } .flex-view-collapsed .phui-badge-flex-item { padding: 1px; } .phui-badge-view { display: inline-block; position: relative; perspective: 512px; } .phui-badge-card-container { transform-style: preserve-3d; -webkit-transform-style: preserve-3d; transition: transform 0.5s; -webkit-transition: -webkit-transform 0.5s; transform-origin: right center; -webkit-transform-origin: right center; width: 100%; height: 100%; } .phui-badge-card { cursor: pointer; padding: 12px; height: 180px; width: 128px; border: 1px solid {$lightgreyborder}; background-color: {$lightbluebackground}; } .card-flipped .phui-badge-card-container { transform: translateX( -100% ) rotateY( -180deg ); -webkit-transform: translateX( -100% ) rotateY( -180deg ); } .phui-badge-card-front, .phui-badge-card-back { display: block; position: absolute; width: 128px; - height: 100%; + height: 180px; + overflow: hidden; backface-visibility: hidden; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; } /* Firefox Fix */ .phui-badge-card-front { transform: rotateY(0deg); -webkit-trasform: rotateY(0deg); } .phui-badge-front-view, .phui-badge-back-view { display: table; width: 100%; } .phui-badge-inner-front, .phui-badge-inner-back { display: table-row; } .phui-badge-illustration { background-color: #fff; box-shadow: inset 0 -1px 1px 0 rgba(0,0,0,.1); text-align: center; height: 100px; width: 100%; vertical-align: middle; display: table-cell; } .phui-badge-illustration .phui-icon-view { font-size: 48px; height: 48px; width: 48px; color: {$bluetext}; } .phui-badge-view-information { display: table-cell; text-align: center; color: {$greytext}; } .phui-badge-view-header { color: {$darkgreytext}; font-weight: bold; padding-top: 12px; display: block; + word-break: break-word; } .phui-badge-quality { padding: 4px 0; color: {$darkbluetext}; font-weight: bold; } .phui-badge-view-subhead { color: {$lightgreytext}; font-size: {$smallerfontsize}; + word-break: break-word; } .phui-badge-card-back { transform: rotateY( 180deg ); -webkit-transform: rotateY( 180deg ); } /** Quality Levels ***********************************************************/ .phui-badge-view-grey .phui-badge-card { background-color: {$lightgreybackground}; } .phui-badge-view-white .phui-badge-card { background-color: {$lightbluebackground}; } .phui-badge-view-green .phui-badge-card { background-color: {$sh-greenbackground}; border-color: {$sh-lightgreenborder}; } .phui-badge-view-blue .phui-badge-card { background-color: {$sh-bluebackground}; border-color: {$sh-lightblueborder}; } .phui-badge-view-indigo .phui-badge-card { background-color: {$sh-indigobackground}; border-color: {$sh-lightindigoborder}; } .phui-badge-view-orange .phui-badge-card { background-color: {$sh-orangebackground}; border-color: {$sh-lightorangeborder}; } .phui-badge-view-yellow .phui-badge-card { background-color: {$sh-yellowbackground}; border-color: {$sh-lightyellowborder}; } /** Mini View ****************************************************************/ .phui-badge-mini { background-color: {$greyborder}; border-radius: 18px; height: 25px; width: 25px; line-height: 24px; text-align: center; display: inline-block; opacity: 0.7; } .phui-badge-mini:hover { opacity: 1; } .phui-badge-mini .phui-icon-view { color: #fff; font-size: 12px; margin: 0; } .phui-badge-mini-grey { background-color: {$greyborder}; } .phui-badge-mini-white { background-color: {$lightblueborder}; } .phui-badge-mini-green { background-color: {$sh-greenicon}; } .phui-badge-mini-blue { background-color: {$blue}; } .phui-badge-mini-indigo { background-color: {$sh-indigoicon}; } .phui-badge-mini-orange { background-color: {$sh-orangeicon}; } .phui-badge-mini-yellow { background-color: {$sh-yellowicon}; } diff --git a/webroot/rsrc/css/phui/phui-feed-story.css b/webroot/rsrc/css/phui/phui-feed-story.css index 4bc505d0c1..9162d68328 100644 --- a/webroot/rsrc/css/phui/phui-feed-story.css +++ b/webroot/rsrc/css/phui/phui-feed-story.css @@ -1,97 +1,96 @@ /** * @provides phui-feed-story-css */ .phui-object-box .phui-box.phui-feed-story { border-bottom: 1px solid {$thinblueborder}; } .phui-object-box .phui-box.phui-feed-story:last-child { border: none; } .phui-feed-story-head .phui-feed-story-actor-image { width: 35px; height: 35px; background-size: 35px; float: left; margin-right: 8px; box-shadow: {$borderinset}; border-radius: 3px; } .phui-feed-story-head { padding: 12px 4px; overflow: hidden; color: {$greytext}; line-height: 16px; word-break: break-word; } .phui-feed-story-head .phui-link-person { color: {$darkgreytext}; font-weight: bold; } .phui-feed-story-body { margin: 4px 4px 8px; color: {$darkgreytext}; word-break: break-word; max-height: 300px; overflow: hidden; } .phui-feed-story-foot { font-size: {$smallerfontsize}; padding: 8px 4px 12px; } .phui-feed-story-foot, .phui-feed-story-foot a { color: {$greytext}; } .phui-feed-story-foot .phui-icon-view { margin-right: 5px; } .phui-feed-story-bigtext-post { line-height: 18px; color: {$darkgreytext}; } .phui-feed-story-bigtext-post h3 { - font-size: 18px; - font-weight: 200; + font-size: {$biggestfontsize}; line-height: 18px; color: {$darkgreytext}; margin: 0 0 5px 0; } .phui-feed-token-bar { margin-top: 8px; border-top: 1px solid #e7e7e7; padding-top: 8px; } .phui-feed-token-bar .phui-icon-view { margin-right: 2px; display: inline-block; } .phui-feed-story-action-list { float: right; padding-top: 4px; } .phui-feed-story-action-item { float: right; padding-left: 2px; height: 18px; width: 18px; font-size: 18px; } .phui-feed-story-action-list .phui-icon-view { display: block; } diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css index 2be3de770d..fe965b1b00 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -1,718 +1,731 @@ /** * @provides phui-object-item-list-view-css */ ul.phui-object-item-list-view { padding: 8px; list-style: none; } .device-desktop .phui-object-item-list-view { padding: 16px; } .phui-object-item-list-view + .phui-object-item-list-view { padding-top: 0; } .phui-object-item-list-view.phui-object-list-flush { padding: 0; margin: 0; } .phui-object-box .phui-object-item-list-view .phui-object-item { margin: 0; } .phui-object-item-list-view .phui-info-view { margin: 0; } .phui-object-box .phui-object-item-list-view .phui-info-view { margin: 4px 0; color: {$greytext}; border: none; } .phui-object-item { border-style: solid; border-color: {$lightgreyborder}; margin: 5px 0; overflow: hidden; background: #fff; margin-bottom: 4px; } .phui-object-item .phui-icon-view { display: inline-block; } .phui-object-item-frame { border-color: {$lightblueborder}; border-width: 1px 1px 1px 0; border-style: solid; position: relative; min-height: 33px; overflow: hidden; } .phui-object-item-no-bar .phui-object-item-frame { border-width: 1px; } .device-desktop .phui-object-item { margin: 0 0 4px 0; } .phui-object-box .phui-object-list-flush .phui-object-item { margin: 0; } .phui-object-box .phui-object-item-list-view { margin: 0; } .phui-object-item-status-icon { font-weight: bold; padding: 3px; font-size: 16px; } .phui-object-item-list-view .phui-object-item-col0 .phui-icon-view { width: 17px; text-align: center; overflow: visible; position: relative; left: -1px; } .phui-object-item-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-object-item-name { overflow: normal; white-space: normal; font-weight: bold; } .phui-object-item-link { display: inline; } .phui-object-item-objname { color: #000; cursor: text; font-weight: bold; } .phui-object-item-content { margin: 4px 8px 2px 0; overflow: hidden; } .phui-object-item-grippable { cursor: move; } .device .phui-object-item-grippable { cursor: normal; } .phui-object-item-grip { position: absolute; top: 0; bottom: 0; left: 0; width: 20px; background: url('/rsrc/image/texture/grip.png') center center no-repeat; } .device .phui-object-item-grip { display: none; } .phui-object-item-grippable .phui-object-item-frame { padding-left: 16px; } .device .phui-object-item-grippable .phui-object-item-frame { padding-left: 0; } .phui-object-item-list-header { padding: 0 0 8px 0; color: {$darkgreytext}; } .phui-object-item-table { display: table; table-layout: fixed; width: 100%; } .phui-object-item-table-row { display: table-row; } .phui-object-item-col0 { width: 20px; display: table-cell; vertical-align: middle; padding-left: 4px; } .device-phone .phui-object-item-col0 { vertical-align: top; padding-top: 8px; } .phui-object-item-col1 { display: table-cell; vertical-align: top; } .phui-object-item-col2 { width: 160px; display: table-cell; vertical-align: top; } .device-phone .phui-object-item-col1, .device-phone .phui-object-item-col2 { display: block; width: auto; } /* - Item Actions -------------------------------------------------------------- Action buttons, like "Edit" and "Delete". */ .phui-object-item-actions { position: absolute; right: 4px; top: 4px; bottom: 4px; vertical-align: middle; text-align: right; } .phui-object-item-actions .phui-list-item-view { float: right; height: 100%; width: 24px; display: inline-block; position: relative; } .phui-object-item-actions .phui-list-item-href { display: inline-block; position: relative; width: 24px; height: 100%; } .device-desktop .phui-object-item-actions .phui-list-item-href:hover { background: {$hoverblue}; border-radius: 3px; } .phui-object-item-actions .phui-list-item-icon { width: 14px; height: 14px; position: absolute; display: block; top: 50%; margin-top: -7px; left: 3px; } .phui-object-item-actions .phui-list-item-name { display: none; } .phui-object-item-with-1-actions .phui-object-item-content-box { margin-right: 28px; overflow: hidden; } .phui-object-item-with-2-actions .phui-object-item-content-box { margin-right: 54px; overflow: hidden; } .phui-object-item-with-3-actions .phui-object-item-content-box { margin-right: 76px; overflow: hidden; } /* - Object Box List ----------------------------------------------------------- Tighter, stacking list when inside an Object Box */ .phui-object-box .phui-object-item-list-view { padding: 0; border: none; } .phui-object-box .phui-object-item-frame { border-right: none; } .phui-object-box .phui-object-item:last-child .phui-object-item-frame { border-bottom: none; } /* - Subhead ------------------------------------------------------------------- Descriptive Text or Links under the main header, before attributes. */ .phui-object-item-subhead { color: {$greytext}; padding: 0 8px 6px; } /* - Attribute List ------------------------------------------------------------ Object attributes, commonly used to render created date, etc. */ .phui-object-item-attributes { padding: 0 8px 6px; line-height: 18px; } .phui-object-item-attribute { display: inline; color: {$greytext}; } .phui-object-item-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-object-item-with-handle-icons .phui-object-item-icons { padding-bottom: 30px; } .phui-object-item-icons { padding: 0 4px 0 0; } .device-phone .phui-object-item-icons { padding: 0 0 0 8px; } ul.phui-object-item-icons { margin: 0; } .phui-object-item-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-object-item-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-object-item-icon .none { display: none; } .phui-object-item-icon-image { width: 14px; height: 14px; font-size: 13px; margin-right: 4px; } /* - Bar Colors ---------------------------------------------------------------- Colors for the left-hand border bars, used to indicate object status or other attributes. */ .phui-workboard-view .phui-object-item { border-left-width: 4px; } .phui-object-item { border-left-width: 0; } .phui-object-item-bar-color-red { border-left-color: {$red}; } .phui-object-item-bar-color-orange { border-left-color: {$orange}; } .phui-object-item-bar-color-yellow { border-left-color: {$yellow}; } .phui-object-item-bar-color-green { border-left-color: {$green}; } .phui-object-item-bar-color-sky { border-left-color: {$sky}; } .phui-object-item-bar-color-blue { border-left-color: {$blue}; } .phui-object-item-bar-color-indigo { border-left-color: {$indigo}; } .phui-object-item-bar-color-violet { border-left-color: {$violet}; } .phui-object-item-bar-color-grey { border-left-color: #bdc3c7; } .phui-object-item-bar-color-black { border-left-color: #333333; } /* - Disabled ------------------------------------------------------------------ Disabled/inactive objects. */ .phui-object-item-disabled .phui-object-item-link, .phui-object-item-disabled .phui-object-item-link a { color: {$lightgreytext}; } .phui-object-item-disabled .phui-object-item-frame { border-color: #d7d7d7; } .phui-object-item-disabled .phui-object-item-objname { color: {$greytext}; text-decoration: line-through; } /* - Effects ------------------------------------------------------------------- Effects like highlighted items. */ .phui-object-item.phui-object-item-highlighted { background: {$sh-yellowbackground}; } ul.phui-object-item-list-view .phui-object-item-highlighted .phui-object-item-frame { border-color: {$sh-yellowborder}; } .phui-object-item-selected { background: {$sh-bluebackground}; } ul.phui-object-item-list-view .phui-object-item-selected .phui-object-item-frame { border-color: {$sh-blueborder}; } /* - Foot Icons ---------------------------------------------------------------- Object counts shown in the footer. */ .phui-object-item-foot-icons { margin-left: 10px; bottom: 0; position: absolute; } .phui-object-item-with-foot-icons .phui-object-item-content, .device-phone .phui-object-item-with-foot-icons .phui-object-item-col2 { padding-bottom: 24px; } .device-phone .phui-object-item-with-foot-icons .phui-object-item-content { padding-bottom: 0; } .phui-object-item-foot-icon { display: inline-block; background: {$lightgreyborder}; color: #ffffff; font-weight: bold; margin-right: 3px; padding: 3px 6px 0; height: 17px; vertical-align: middle; position: relative; font-size: 12px; -webkit-font-smoothing: antialiased; } .phui-object-item-foot-icon .phui-icon-view { margin-right: 4px; } /* - Handle Icons -------------------------------------------------------------- Shows owners, reviewers, etc., using profile picture icons. */ .phui-object-item-handle-icons { height: 28px; margin-right: 10px; bottom: 0; right: 0; text-align: right; position: absolute; } .phui-object-item-handle-icon { margin: 1px; width: 28px; height: 28px; display: inline-block; background-size: 28px 28px; background-repeat: no-repeat; } /* - Bylines ------------------------------------------------------------------- Shows owners, authors, reviewers, etc., in text. */ .phui-object-item-bylines { padding: 0 4px 0 8px; margin: 4px 0 8px; font-size: {$smallerfontsize}; color: {$greytext}; text-align: right; } .phui-object-item-byline { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .device-phone .phui-object-item-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 { position: relative; background: {$sh-yellowbackground}; opacity: 0.9; } .drag-sending { opacity: 0.5; } /* - State --------------------------------------------------------------------- Provides a list of object status or states, success or fail, etc */ .phui-object-item-ficon { width: 48px; height: 26px; margin-top: 12px; position: absolute; text-align: center; font-size: 24px; } .phui-object-item-with-ficon .phui-object-item-content-box { margin-left: 38px; } .phui-object-box .phui-object-list-states { padding: 0; } .phui-object-list-states .phui-info-view { margin: 0; border: none; } /* - Badges ---------------------------------------------------------------- */ .phui-object-item-col0.phui-object-item-badge { width: 28px; } .phui-object-item-col0.phui-object-item-badge .phui-icon-view { left: 0; } +/* - Countdowns ------------------------------------------------------------ */ + +.phui-object-item-col0.phui-object-item-countdown { + width: 52px; + padding: 0; +} + +.phui-object-item-countdown .phui-object-item-countdown-number { + border-right: 1px solid {$thinblueborder}; + text-align: center; + color: {$bluetext}; +} + /* - Dashboards ------------------------------------------------------------ */ .phui-object-box .phui-object-item-list-view .phui-object-item-frame { border: none; border-bottom: 1px solid {$thinblueborder}; } .phui-object-box .phui-object-item-list-header { font-size: {$normalfontsize}; color: {$darkbluetext}; border-top: 1px solid {$thinblueborder}; border-bottom: 1px solid {$thinblueborder}; padding: 8px; background-color: {$lightgreybackground}; } .phui-object-box .phui-header-shell + .phui-object-item-list-view .phui-object-item-list-header, .phui-object-box .phui-object-box-hidden-content + .phui-object-item-list-view .phui-object-item-list-header, .phui-object-box .phui-object-box-hidden-content + .phui-object-item-list-header { border-top: none; } .dashboard-pane .phui-object-item-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-object-item-col2 { display: none; } /* - Launcher List ---------------------------------------------------------- */ .phui-object-item-image-icon { background: none; width: 30px; height: 30px; margin: 4px 0; position: absolute; } .phui-object-item-image-icon .phui-icon-view { position: absolute; width: 24px; height: 24px; left: 6px; top: 10px; font-size: 24px; text-align: center; vertical-align: bottom; } .phui-object-item-with-image-icon .phui-object-item-frame { min-height: 48px; } .phui-object-item-with-image-icon .phui-object-item-content-box { margin-left: 36px; } .device-desktop .phui-object-item-launcher-list .phui-object-item-content { margin-right: 0; } .device-desktop .phui-object-item-launcher-list .phui-object-icon-pane { width: auto; } .phui-object-item-image { width: 40px; height: 40px; background-size: 100%; margin: 6px; position: absolute; } .phui-object-item-with-image .phui-object-item-frame { min-height: 52px; } .phui-object-item-with-image .phui-object-item-content-box { margin-left: 46px; } diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css index b4acc54994..71a096e69e 100644 --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -1,203 +1,202 @@ /** * @provides phui-property-list-view-css */ .phui-property-list-view .keyboard-shortcuts-available { float: right; height: 16px; margin: 12px 10px -28px 0px; padding: 0px 20px 0px 0px; vertical-align: middle; color: {$greytext}; text-align: right; font-size: {$smallestfontsize}; background: url('/rsrc/image/icon/fatcow/key_question.png') right center no-repeat; } .device .keyboard-shortcuts-available { display: none; } .phui-property-group-noninitial, .phui-property-list-section-noninitial { border-color: {$thinblueborder}; border-style: solid; border-width: 1px 0 0; } .device-desktop .phui-property-list-container { padding: 12px 0 12px 0; width: 100%; } .device .phui-property-list-container { padding: 12px 0 4px 0; } .phui-property-list-key { color: {$bluetext}; font-weight: bold; overflow: hidden; white-space: nowrap; } .device-desktop .phui-property-list-key { width: 18%; margin-left: 1%; text-align: right; float: left; clear: left; margin-bottom: 4px; } .phui-property-list-properties-wrap.phui-property-list-stacked { width: auto; float: none; } .device .phui-property-list-key, .phui-property-list-stacked .phui-property-list-properties .phui-property-list-key { padding-left: 4px; text-align: left; margin-left: 0; width: auto; float: none; } .phui-property-list-value { color: {$darkgreytext}; - line-height: 17px; } .device-desktop .phui-property-list-value { width: 78%; margin-left: 1%; float: left; margin-bottom: 4px; } .device .phui-property-list-value, .phui-property-list-stacked .phui-property-list-properties .phui-property-list-value { padding: 0 8px; margin-bottom: 8px; width: auto; word-break: break-word; float: none; } .phui-property-list-section-header { color: {$bluetext}; padding: 16px 4px 0px; text-transform: uppercase; font-weight: 700; border-color: {$thinblueborder}; border-style: solid; border-width: 1px 0 0; } .phui-property-list-container + .phui-property-list-text-content { border-color: {$thinblueborder}; border-style: solid; border-width: 1px 0 0; } .phui-property-list-section-noninitial .phui-property-list-section-header { border-top: none; } .device .phui-property-list-section-header { padding-left: 4px; } .phui-property-list-section-header-icon .phui-icon-view { display: inline-block; margin: -2px 4px -2px 0; } .phui-property-list-text-content { padding: 12px 4px; overflow: hidden; } .phui-property-list-raw-content { padding: 0px; overflow: hidden; } /* In the common case where we immediately follow a header, move back up 30px so we snuggle next to the header. */ .device-desktop .phui-header-view + .phabricator-action-list-view { margin-top: -30px; } .device-desktop .phui-header-view + .phabricator-action-list-view + .phui-property-list-view { margin-top: 0px; } .phui-property-list-image { margin: auto; max-width: 95%; } .phui-property-list-audio { display: block; margin: 16px auto; width: 50%; min-width: 240px; } /* When tags appear in property lists, give them a little more vertical spacing. */ .phui-property-list-view .phui-tag-view { margin: 2px 0; } .phui-property-list-properties-wrap { float: left; width: 78%; } .device .phui-property-list-properties-wrap { width: auto; border: none; float: none; } .phui-property-list-actions { width: 20%; float: right; margin-right: 12px; border-left: 1px solid {$thinblueborder}; } .device .phui-property-list-actions { float: none; width: auto; margin: -12px 0 12px 0; border: none; } .phui-property-list-image-content img { margin: 20px auto; background: url('/rsrc/image/checker_light.png'); border: 1px solid {$lightblueborder}; } .device-desktop .phui-property-list-image-content img:hover { background: url('/rsrc/image/checker_dark.png'); } /* - Dashboards ------------------------------------------------------------ */ .dashboard-panel .phui-property-list-section { border-left: 1px solid {$lightblueborder}; border-right: 1px solid {$lightblueborder}; border-bottom: 1px solid {$blueborder}; } diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index fdef1d07a3..de6658f7dc 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -1,397 +1,407 @@ /** * @provides phui-timeline-view-css */ .phui-timeline-view { padding: 0 16px; background-image: url('/rsrc/image/BFCFDA.png'); background-repeat: repeat-y; background-position: 94px; } .device .phui-timeline-view { padding: 0 8px; } .device-tablet .phui-timeline-view { background-position: 24px; } .device-phone .phui-timeline-view { padding: 0; background-position: 24px; } .phui-timeline-major-event .phui-timeline-group { border-left: 1px solid {$lightblueborder}; border-right: 1px solid {$lightblueborder}; border-radius: 3px; } .device-desktop .phui-timeline-event-view { margin-left: 62px; position: relative; } .device-desktop .phui-timeline-event-view.phui-timeline-minor-event { margin-left: 65px; } .device-desktop .phui-timeline-spacer { min-height: 16px; } .device-desktop .phui-timeline-event-view.the-worlds-end { background: {$lightblueborder}; width: 9px; height: 9px; border-radius: 2px; margin-left: 74px; } .device-desktop .phui-timeline-wedge { border-bottom: 1px solid {$lightblueborder}; position: absolute; width: 12px; } .device-phone .phui-timeline-minor-event, .device-tablet .phui-timeline-minor-event { padding-left: 3px; } .phui-timeline-major-event .phui-timeline-content { border-top: 1px solid {$lightblueborder}; border-bottom: 1px solid {$lightblueborder}; border-radius: 3px; } .phui-timeline-title { line-height: 22px; min-height: 19px; position: relative; color: {$bluetext}; } .phui-timeline-minor-event .phui-timeline-title { padding: 4px 8px 4px 33px; } .phui-timeline-title a { font-weight: bold; color: {$darkbluetext}; } .device-desktop .phui-timeline-wedge { left: -12px; } .device-desktop .phui-timeline-major-event .phui-timeline-wedge { top: 26px; } .device-desktop .phui-timeline-minor-event .phui-timeline-wedge { top: 13px; left: -18px; width: 20px; } .phui-timeline-image { background-repeat: no-repeat; position: absolute; border-radius: 3px; box-shadow: {$borderinset}; background-size: 100%; } .device-desktop .phui-timeline-major-event .phui-timeline-image { width: 50px; height: 50px; top: 0px; left: -62px; } .device-desktop .phui-timeline-minor-event .phui-timeline-image { width: 28px; height: 28px; background-size: 28px auto; left: -41px; } .phui-timeline-major-event .phui-timeline-title { background: {$lightbluebackground}; min-height: 22px; border-top-right-radius: 3px; } .phui-timeline-title + .phui-timeline-title { border-radius: 0; } .phui-timeline-title { padding: 5px 8px; overflow-x: auto; overflow-y: hidden; } .phui-timeline-title-with-icon { padding-left: 38px; } .phui-timeline-title-with-menu { padding-right: 36px; } .phui-timeline-view .phui-icon-view.phui-timeline-token { vertical-align: middle; margin-right: 4px; } .phui-timeline-token.strikethrough { position: relative; } .phui-timeline-token.strikethrough:before { position: absolute; content: ""; left: 0; top: 50%; right: 0; border-top: 3px solid; border-color: {$darkbluetext}; -webkit-transform:rotate(-40deg); -moz-transform:rotate(-40deg); -ms-transform:rotate(-40deg); -o-transform:rotate(-40deg); transform:rotate(-40deg); } .phui-timeline-major-event .phui-timeline-content .phui-timeline-core-content { padding: 16px 12px; line-height: 18px; background: #fff; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } .phui-timeline-core-content { overflow-x: auto; } .phui-timeline-core-content .comment-deleted { font-style: italic; } .device .phui-timeline-event-view { min-height: 23px; position: relative; } .device-phone .phui-timeline-event-view { margin: 0 8px; } .device .phui-timeline-image { display: none; } .device .phui-timeline-spacer { min-height: 8px; border-width: 0; } .phui-timeline-spacer.phui-timeline-spacer-bold { border-bottom: 4px solid {$lightblueborder}; margin: 0; } .phui-timeline-spacer-bold + .phui-timeline-spacer { background-color: #ebecee; } .phui-timeline-icon-fill { position: absolute; width: 32px; height: 32px; background-color: {$lightblueborder}; top: 0; left: 0; text-align: center; } .phui-icon-view.phui-timeline-icon:before { font-size: 15px; } .phui-timeline-minor-event .phui-timeline-icon-fill { height: 28px; width: 28px; border-radius: 3px; } .phui-timeline-icon-fill .phui-timeline-icon { margin-top: 8px; } .phui-timeline-minor-event .phui-timeline-icon-fill .phui-timeline-icon { margin-top: 7px; } .phui-timeline-extra, .phui-timeline-extra .phabricator-content-source-view { font-size: {$smallestfontsize}; font-weight: normal; color: {$lightbluetext}; } .phui-timeline-title .phui-timeline-extra a { font-weight: normal; } .device-desktop .phui-timeline-extra { float: right; } .device .phui-timeline-extra { display: inline-block; line-height: 16px; margin-left: 8px; white-space: nowrap; } .device-phone .phui-timeline-extra { display: block; margin: 0; } .phui-timeline-icon-fill-red { background-color: {$red}; } .phui-timeline-icon-fill-orange { background-color: {$orange}; } .phui-timeline-icon-fill-yellow { background-color: {$yellow}; } .phui-timeline-icon-fill-green { background-color: {$green}; } .phui-timeline-icon-fill-sky { background-color: {$sky}; } .phui-timeline-icon-fill-blue { background-color: {$blue}; } .phui-timeline-icon-fill-indigo { background-color: {$indigo}; } .phui-timeline-icon-fill-violet { background-color: {$violet}; } .phui-timeline-icon-fill-grey { background-color: #888; } .phui-timeline-icon-fill-black { background-color: #333; } .phui-timeline-shell.anchor-target { background: {$lightyellow}; padding: 4px; margin: -4px; } .phui-timeline-preview-header { background: #e0e3ec; color: {$darkgreytext}; padding: 4px 1.25%; border: solid {$blueborder} 1px 0; } .phui-timeline-change-details { padding: 10px 0; border-style: solid; border-color: #efefef; border-width: 1px 0; } .phui-timeline-older-transactions-are-hidden { background: {$sh-yellowbackground}; border: 1px solid {$sh-yellowborder}; text-align: center; padding: 12px; color: {$darkgreytext}; cursor: pointer; border-radius: 3px; } .device-phone .phui-timeline-older-transactions-are-hidden { margin: 0 8px; } .phui-timeline-title .phui-timeline-extra-information a { font-weight: normal; color: {$bluetext}; } .phui-timeline-comment-actions .phui-icon-view { width: 16px; height: 16px; font-size: 16px; text-align: center; overflow: hidden; } .phui-timeline-menu { position: absolute; right: 3px; top: 6px; width: 28px; height: 22px; text-align: center; line-height: 22px; font-size: 15px; border-left: 1px solid {$lightblueborder}; } .phui-timeline-menu:focus { outline: none; } .phui-timeline-menu .phui-icon-view { color: {$lightgreytext}; } a.phui-timeline-menu .phui-icon-view { color: {$bluetext}; } .device-desktop a.phui-timeline-menu:hover .phui-icon-view { color: {$darkgreytext}; } .phui-timeline-menu.phuix-dropdown-open { background: {$hovergrey}; } .phui-timeline-view + .phui-object-box { margin-top: 0; } .phui-timeline-badges { position: absolute; left: -64px; top: 52px; width: 54px; } + +.phui-timeline-badges .phui-badge-mini { + height: 18px; + width: 18px; + line-height: 16px; +} + +.phui-timeline-badges .phui-badge-mini .phui-icon-view { + font-size: 10px; +} diff --git a/webroot/rsrc/css/phui/phui-workboard-view.css b/webroot/rsrc/css/phui/phui-workboard-view.css index 7299059496..2befbd7823 100644 --- a/webroot/rsrc/css/phui/phui-workboard-view.css +++ b/webroot/rsrc/css/phui/phui-workboard-view.css @@ -1,140 +1,146 @@ /** * @provides phui-workboard-view-css */ .phui-workboard-view { width: 100%; } .device-desktop .phui-workboard-view-shadow { overflow-x: auto; position: absolute; top: 96px; bottom: 0; left: 0; right: 0; } .device-desktop .page-has-warning .phui-workboard-view-shadow { top: 132px; } .device-desktop.with-durable-column .phui-workboard-view-shadow { right: 300px; } .device-desktop.with-durable-margin .phui-workboard-view-shadow { right: 312px; } .phui-workboard-view-shadow::-webkit-scrollbar { height: 12px; width: 12px; background: rgba(200,200,200,.6); } .phui-workboard-view-shadow::-webkit-scrollbar-thumb { background: {$lightbluetext}; } .phui-workboard-action-list { width: 60px; float: left; min-height: 60px; border-radius: 5px; margin-right: 8px; background: {$lightbluebackground}; border: 1px solid {$lightblueborder}; border-bottom: 1px solid {$blueborder}; } .device-phone .phui-workboard-action-list { width: 100%; float: none; display: block; overflow: hidden; border: none; background: none; border-radius: none; min-height: 0; } .phui-workboard-action-list li:first-child { padding-top: 5px; } .device-phone .phui-workboard-action-list li { display: inline; float: left; margin: 0; padding: 0 0 8px 0; } .phui-workboard-action-list .phui-icon-view { display: inline-block; vertical-align: top; width: 50px; height: 50px; margin: 0 4px 5px 5px; } .device-phone .phui-workboard-action-list .phui-icon-view { background-size: 35px; height: 35px; width: 35px; margin: 0 3px; } .device-desktop .project-board-wrapper .phui-workboard-view-shadow { left: 53px; } .device-desktop .phui-workboard-view .aphront-multi-column-fixed .aphront-multi-column-inner { margin-left: 0; } .device-tablet .project-board-wrapper { margin-left: 8px; margin-right: 8px; } .project-board-header { background-color: #fff; border-bottom: 1px solid {$lightblueborder}; padding: 12px; position: relative; } .device .project-board-header { padding: 0; } .project-board-header .phui-header-shell { padding: 0; border-bottom: 1px solid {$blueborder}; } .device .project-board-header .phui-header-shell { padding: 8px; } .project-board-header .phui-header-header { font-size: 18px; } .project-board-header .phui-header-subheader { display: inline-block; margin: 0; padding: 0 8px; } +.device-phone .project-board-header .phui-header-subheader { + display: block; + margin: 8px 0 2px 0; + padding: 0; +} + .device-desktop .phabricator-icon-nav.project-board-nav .phabricator-nav-local { margin-top: 64px; } .device-desktop .phabricator-icon-nav.project-board-nav .phabricator-nav-content { margin: 0; } diff --git a/webroot/rsrc/css/phui/phui-workpanel-view.css b/webroot/rsrc/css/phui/phui-workpanel-view.css index b042bda0e2..9b8943ceeb 100644 --- a/webroot/rsrc/css/phui/phui-workpanel-view.css +++ b/webroot/rsrc/css/phui/phui-workpanel-view.css @@ -1,145 +1,150 @@ /** * @provides phui-workpanel-view-css */ .phui-workpanel-view .phui-header-shell { padding: 16px 0 8px; border-color: {$lightgreyborder}; width: 300px; background-color: transparent; } .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}; } .device-phone .phui-workboard-view { width: auto; margin: 0 8px; } .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-workboard-view .phui-object-item .phui-object-item-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-workboard-view .phui-object-item-link { white-space: normal; display: inline; -webkit-font-smoothing: antialiased; } .phui-workboard-view .phui-object-item-objname { vertical-align: top; } .phui-workpanel-view .phui-workpanel-header-action { float: right; width: 24px; border-left: 1px solid #b3b5b6; } .phui-workpanel-view .phui-workpanel-body { padding-top: 8px; } .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; } .phui-workpanel-view .phui-list-item-icon { height: 14px; width: 14px; display: inline-block; } .phui-workpanel-view .phui-list-item-name { padding-left: 5px; display: inline-block; } .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-object-item-list-view { min-height: 54px; } .device .aphront-multi-column-outer div.aphront-multi-column-column-outer .phui-workpanel-body { width: auto; } .project-panel-hidden { opacity: 0.75; } .project-panel-empty .phui-object-item-list-view { background: {$sh-indigobackground}; border-radius: 3px; margin-bottom: 4px; border: 1px dashed {$sh-indigoborder}; } .project-panel-empty .phui-object-item-list-view .drag-ghost { display: none; } .project-panel-empty .phui-object-item-list-view.drag-target-list { background: rgba(255,255,255,.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}; } /* - Workpanel Cards ----------------------------------------------------------- Slight display changes for how cards work in tight spaces */ .phui-workpanel-view .phui-object-item-grippable .phui-object-item-frame { padding-left: 0; } .phui-workpanel-view .phui-object-item-grip { display: none; } .phui-workpanel-view .phui-object-item-attribute a { color: {$bluetext}; } diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.eot b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.eot index 33b2bb8005..a30335d748 100644 Binary files a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.eot and b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.eot differ diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf index ed9372f8ea..d7994e1308 100644 Binary files a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf and b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf differ diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff index 8b280b98fa..6fd4ede0f3 100644 Binary files a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff and b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff differ diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2 b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2 index 3311d58514..5560193ccc 100644 Binary files a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2 and b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2 differ diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js index bf1db41dfc..82100062ed 100644 --- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -1,384 +1,394 @@ /** * @requires multirow-row-manager * javelin-install * javelin-util * javelin-dom * javelin-stratcom * javelin-json * phabricator-prefab * @provides herald-rule-editor * @javelin */ JX.install('HeraldRuleEditor', { construct : function(config) { var root = JX.$(config.root); this._root = root; JX.DOM.listen( root, 'click', 'create-condition', JX.bind(this, this._onnewcondition)); JX.DOM.listen( root, 'click', 'create-action', JX.bind(this, this._onnewaction)); JX.DOM.listen(root, 'change', null, JX.bind(this, this._onchange)); JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit)); var conditionsTable = JX.DOM.find(root, 'table', 'rule-conditions'); var actionsTable = JX.DOM.find(root, 'table', 'rule-actions'); this._conditionsRowManager = new JX.MultirowRowManager(conditionsTable); this._conditionsRowManager.listen( 'row-removed', JX.bind(this, function(row_id) { delete this._config.conditions[row_id]; })); this._actionsRowManager = new JX.MultirowRowManager(actionsTable); this._actionsRowManager.listen( 'row-removed', JX.bind(this, function(row_id) { delete this._config.actions[row_id]; })); this._conditionGetters = {}; this._conditionTypes = {}; this._actionGetters = {}; this._actionTypes = {}; this._config = config; var conditions = this._config.conditions; this._config.conditions = []; var actions = this._config.actions; this._config.actions = []; this._renderConditions(conditions); this._renderActions(actions); }, members : { _config : null, _root : null, _conditionGetters : null, _conditionTypes : null, _actionGetters : null, _actionTypes : null, _conditionsRowManager : null, _actionsRowManager : null, _onnewcondition : function(e) { this._newCondition(); e.kill(); }, _onnewaction : function(e) { this._newAction(); e.kill(); }, _onchange : function(e) { var target = e.getTarget(); var row = e.getNode(JX.MultirowRowManager.getRowSigil()); if (!row) { // Changing the "when all of / any of these..." dropdown. return; } if (JX.Stratcom.hasSigil(target, 'field-select')) { this._onfieldchange(row); } else if (JX.Stratcom.hasSigil(target, 'condition-select')) { this._onconditionchange(row); } else if (JX.Stratcom.hasSigil(target, 'action-select')) { this._onactionchange(row); } }, _onsubmit : function() { var rule = JX.DOM.find(this._root, 'input', 'rule'); var k; for (k in this._config.conditions) { this._config.conditions[k][2] = this._getConditionValue(k); } for (k in this._config.actions) { this._config.actions[k][1] = this._getActionTarget(k); } rule.value = JX.JSON.stringify({ conditions: this._config.conditions, actions: this._config.actions }); }, _getConditionValue : function(id) { if (this._conditionGetters[id]) { return this._conditionGetters[id](); } return this._config.conditions[id][2]; }, _getActionTarget : function(id) { if (this._actionGetters[id]) { return this._actionGetters[id](); } return this._config.actions[id][1]; }, _onactionchange : function(r) { var target = JX.DOM.find(r, 'select', 'action-select'); var row_id = this._actionsRowManager.getRowID(r); this._config.actions[row_id][0] = target.value; var target_cell = JX.DOM.find(r, 'td', 'target-cell'); var target_input = this._renderTargetInputForRow(row_id); JX.DOM.setContent(target_cell, target_input); }, _onfieldchange : function(r) { var target = JX.DOM.find(r, 'select', 'field-select'); var row_id = this._actionsRowManager.getRowID(r); this._config.conditions[row_id][0] = target.value; var condition_cell = JX.DOM.find(r, 'td', 'condition-cell'); var condition_select = this._renderSelect( this._selectKeys( this._config.info.conditions, this._config.info.conditionMap[target.value]), this._config.conditions[row_id][1], 'condition-select'); JX.DOM.setContent(condition_cell, condition_select); this._onconditionchange(r); var condition_name = this._config.conditions[row_id][1]; if (condition_name == 'unconditionally') { JX.DOM.hide(condition_select); } }, _onconditionchange : function(r) { var target = JX.DOM.find(r, 'select', 'condition-select'); var row_id = this._conditionsRowManager.getRowID(r); this._config.conditions[row_id][1] = target.value; var value_cell = JX.DOM.find(r, 'td', 'value-cell'); var value_input = this._renderValueInputForRow(row_id); JX.DOM.setContent(value_cell, value_input); }, _renderTargetInputForRow : function(row_id) { var action = this._config.actions[row_id]; var type = this._config.info.targets[action[0]]; var input = this._buildInput(type); var node = input[0]; var get_fn = input[1]; var set_fn = input[2]; if (node) { JX.Stratcom.addSigil(node, 'action-target'); } var old_type = this._actionTypes[row_id]; if (old_type == type || !old_type) { set_fn(this._getActionTarget(row_id)); } this._actionTypes[row_id] = type; this._actionGetters[row_id] = get_fn; return node; }, _buildInput : function(type) { var spec = this._config.info.valueMap[type]; var input; var get_fn; var set_fn; switch (spec.control) { case 'herald.control.none': input = null; get_fn = JX.bag; set_fn = JX.bag; break; case 'herald.control.text': input = JX.$N('input', {type: 'text'}); get_fn = function() { return input.value; }; set_fn = function(v) { input.value = v; }; break; case 'herald.control.select': var options; // NOTE: This is a hacky special case for "Another Herald Rule", // which we don't currently generate normal options for. if (spec.key == 'select.rule') { options = this._config.template.rules; } else { options = spec.template.options; } input = this._renderSelect(options); get_fn = function() { return input.value; }; set_fn = function(v) { input.value = v; }; if (spec.template.default) { set_fn(spec.template.default); } break; case 'herald.control.tokenizer': var tokenizer = this._newTokenizer(spec.template.tokenizer); input = tokenizer[0]; get_fn = tokenizer[1]; set_fn = tokenizer[2]; break; default: JX.$E('No rules to build control "' + spec.control + '".'); break; } return [input, get_fn, set_fn]; }, _renderValueInputForRow : function(row_id) { var cond = this._config.conditions[row_id]; var type = this._config.info.values[cond[0]][cond[1]]; var input = this._buildInput(type); var node = input[0]; var get_fn = input[1]; var set_fn = input[2]; if (node) { JX.Stratcom.addSigil(node, 'condition-value'); } var old_type = this._conditionTypes[row_id]; if (old_type == type || !old_type) { set_fn(this._getConditionValue(row_id)); } this._conditionTypes[row_id] = type; this._conditionGetters[row_id] = get_fn; return node; }, _newTokenizer : function(spec) { var tokenizerConfig = { src: spec.datasourceURI, placeholder: spec.placeholder, browseURI: spec.browseURI }; var build = JX.Prefab.newTokenizerFromTemplate( this._config.template.markup, tokenizerConfig); build.tokenizer.start(); return [ build.node, function() { return build.tokenizer.getTokens(); }, function(map) { for (var k in map) { build.tokenizer.addToken(k, map[k]); } }]; }, _selectKeys : function(map, keys) { var r = {}; for (var ii = 0; ii < keys.length; ii++) { r[keys[ii]] = map[keys[ii]]; } return r; }, _renderConditions : function(conditions) { for (var k in conditions) { this._newCondition(conditions[k]); } }, _newCondition : function(data) { var row = this._conditionsRowManager.addRow([]); var row_id = this._conditionsRowManager.getRowID(row); this._config.conditions[row_id] = data || [null, null, '']; var r = this._conditionsRowManager.updateRow( row_id, this._renderCondition(row_id)); this._onfieldchange(r); }, _renderCondition : function(row_id) { var groups = this._config.info.fields; - var optgroups = []; - for (var ii = 0; ii < groups.length; ii++) { - var group = groups[ii]; - var options = []; - for (var k in group.options) { - options.push(JX.$N('option', {value: k}, group.options[k])); - } - optgroups.push(JX.$N('optgroup', {label: group.label}, options)); - } - var attrs = { sigil: 'field-select' }; - var field_select = JX.$N('select', attrs, optgroups); + var field_select = this._renderGroupSelect(groups, attrs); field_select.value = this._config.conditions[row_id][0]; var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select); var condition_cell = JX.$N('td', {sigil: 'condition-cell'}); var value_cell = JX.$N('td', {className : 'value', sigil: 'value-cell'}); return [field_cell, condition_cell, value_cell]; }, _renderActions : function(actions) { for (var k in actions) { this._newAction(actions[k]); delete actions[k]; } }, + + _renderGroupSelect: function(groups, attrs) { + var optgroups = []; + for (var ii = 0; ii < groups.length; ii++) { + var group = groups[ii]; + var options = []; + for (var k in group.options) { + options.push(JX.$N('option', {value: k}, group.options[k])); + } + optgroups.push(JX.$N('optgroup', {label: group.label}, options)); + } + + return JX.$N('select', attrs, optgroups); + }, + _newAction : function(data) { data = data || []; var temprow = this._actionsRowManager.addRow([]); var row_id = this._actionsRowManager.getRowID(temprow); this._config.actions[row_id] = data; var r = this._actionsRowManager.updateRow(row_id, this._renderAction(data)); this._onactionchange(r); }, + _renderAction : function(action) { - var action_select = this._renderSelect( - this._config.info.actions, - action[0], - 'action-select'); + var groups = this._config.info.actions; + var attrs = { + sigil: 'action-select' + }; + + var action_select = this._renderGroupSelect(groups, attrs); + action_select.value = action[0]; + var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select); var target_cell = JX.$N( 'td', {className : 'target', sigil : 'target-cell'}); return [action_cell, target_cell]; }, _renderSelect : function(map, selected, sigil) { var attrs = { sigil : sigil }; return JX.Prefab.renderSelect(map, selected, attrs); } } });