diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b57104b888..2f2f83307f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -1,2343 +1,2343 @@ array( 'core.pkg.css' => 'c2c68e64', - 'core.pkg.js' => 'f8ec7ddc', + 'core.pkg.js' => '2b9e8efd', 'darkconsole.pkg.js' => 'df001cab', 'differential.pkg.css' => '4a93db37', 'differential.pkg.js' => '7528cfc9', 'diffusion.pkg.css' => '471bc9eb', 'diffusion.pkg.js' => 'bfc0737b', 'maniphest.pkg.css' => 'f5d89daf', 'maniphest.pkg.js' => 'df4aa49f', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/context-bar.css' => '1c3b0529', 'rsrc/css/aphront/dark-console.css' => '6378ef3d', 'rsrc/css/aphront/dialog-view.css' => '4dbbe3bb', 'rsrc/css/aphront/error-view.css' => '3462dbee', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => '2ae43867', 'rsrc/css/aphront/multi-column.css' => '1b95ab2e', 'rsrc/css/aphront/notification.css' => 'ef2c9b34', 'rsrc/css/aphront/pager-view.css' => '2e3539af', 'rsrc/css/aphront/panel-view.css' => '5846dfa2', 'rsrc/css/aphront/phabricator-nav-view.css' => '9283c2df', 'rsrc/css/aphront/request-failure-view.css' => 'da14df31', 'rsrc/css/aphront/table-view.css' => 'b22b7216', 'rsrc/css/aphront/tokenizer.css' => '82ce2142', 'rsrc/css/aphront/tooltip.css' => '9c90229d', 'rsrc/css/aphront/transaction.css' => '5d0cae25', 'rsrc/css/aphront/two-column.css' => '16ab3ad2', 'rsrc/css/aphront/typeahead.css' => 'a989b5b3', 'rsrc/css/application/auth/auth.css' => '1e655982', 'rsrc/css/application/base/main-menu-view.css' => 'aceca0e9', 'rsrc/css/application/base/notification-menu.css' => '8ae4a008', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '8b7e271d', 'rsrc/css/application/base/standard-page-view.css' => '517cdfb1', 'rsrc/css/application/chatlog/chatlog.css' => '852140ff', 'rsrc/css/application/config/config-options.css' => '7fedf08b', 'rsrc/css/application/config/config-template.css' => '25d446d6', 'rsrc/css/application/config/config-welcome.css' => 'b0d16200', 'rsrc/css/application/config/setup-issue.css' => '69e640e7', 'rsrc/css/application/conpherence/menu.css' => 'e1e0fdf1', 'rsrc/css/application/conpherence/message-pane.css' => '11a393ca', 'rsrc/css/application/conpherence/notification.css' => '04a6e10a', 'rsrc/css/application/conpherence/update.css' => '1099a660', 'rsrc/css/application/conpherence/widget-pane.css' => 'bf275a6c', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '86b7b0a0', 'rsrc/css/application/dashboard/dashboard.css' => 'a2bfdcbf', 'rsrc/css/application/diff/inline-comment-summary.css' => '8cfd34e8', 'rsrc/css/application/differential/add-comment.css' => 'c478bcaa', 'rsrc/css/application/differential/changeset-view.css' => 'ff8eacf8', 'rsrc/css/application/differential/core.css' => '7ac3cabc', 'rsrc/css/application/differential/results-table.css' => '239924f9', 'rsrc/css/application/differential/revision-comment.css' => '48186045', 'rsrc/css/application/differential/revision-history.css' => '0e8eb855', 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33', 'rsrc/css/application/differential/table-of-contents.css' => '6bf8e1d2', 'rsrc/css/application/diffusion/commit-view.css' => '92d1e8f9', 'rsrc/css/application/diffusion/diffusion-icons.css' => '9c5828da', 'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661', 'rsrc/css/application/feed/feed.css' => '4e544db4', 'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad', 'rsrc/css/application/flag/flag.css' => '5337623f', 'rsrc/css/application/harbormaster/harbormaster.css' => 'cec833b7', 'rsrc/css/application/herald/herald-test.css' => '778b008e', 'rsrc/css/application/herald/herald.css' => 'c544dd1c', 'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc', 'rsrc/css/application/maniphest/report.css' => '6fc16517', 'rsrc/css/application/maniphest/task-edit.css' => '8e23031b', 'rsrc/css/application/maniphest/task-summary.css' => '00c3be7a', 'rsrc/css/application/objectselector/object-selector.css' => '029a133d', 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => 'aa1767d1', 'rsrc/css/application/people/people-profile.css' => 'ba7b2762', 'rsrc/css/application/phame/phame.css' => '19ecc703', 'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => '47dffb9c', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => 'b25b4beb', 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad', 'rsrc/css/application/phriction/phriction-document-css.css' => '7d7f0071', 'rsrc/css/application/policy/policy-edit.css' => '05cca26a', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/comments.css' => '6cdccea7', 'rsrc/css/application/ponder/feed.css' => 'e62615b6', 'rsrc/css/application/ponder/post.css' => 'ebab8a70', 'rsrc/css/application/ponder/vote.css' => '8ed6ed8b', 'rsrc/css/application/profile/profile-view.css' => 'b459416e', 'rsrc/css/application/projects/project-icon.css' => 'c2ecb7f1', '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' => 'f240504c', 'rsrc/css/application/slowvote/slowvote.css' => '266df6a1', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '40151074', 'rsrc/css/core/remarkup.css' => 'ad4c0676', 'rsrc/css/core/syntax.css' => '3c18c1cb', 'rsrc/css/core/z-index.css' => 'd1c137f2', 'rsrc/css/diviner/diviner-shared.css' => '38813222', 'rsrc/css/font/font-awesome.css' => '73d075c3', 'rsrc/css/font/font-source-sans-pro.css' => '91d53463', 'rsrc/css/font/phui-font-icon-base.css' => 'eb84f033', 'rsrc/css/layout/phabricator-action-header-view.css' => '83e2cc86', 'rsrc/css/layout/phabricator-crumbs-view.css' => '7fbf25b8', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 'rsrc/css/layout/phabricator-hovercard-view.css' => '893f4783', 'rsrc/css/layout/phabricator-side-menu-view.css' => 'a2ccd7bd', 'rsrc/css/layout/phabricator-source-code-view.css' => '7d346aa4', 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'de035c8a', 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1d0ca59', 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'a92e47d2', 'rsrc/css/phui/calendar/phui-calendar.css' => '5e1ad989', 'rsrc/css/phui/phui-action-list.css' => '9ee9910a', 'rsrc/css/phui/phui-box.css' => '7b3a2eed', 'rsrc/css/phui/phui-button.css' => 'c7412aa1', 'rsrc/css/phui/phui-document.css' => 'a5615198', 'rsrc/css/phui/phui-feed-story.css' => 'e2c9bc83', 'rsrc/css/phui/phui-fontkit.css' => 'abeb59f0', 'rsrc/css/phui/phui-form-view.css' => 'ebac1b1d', 'rsrc/css/phui/phui-form.css' => 'b78ec020', 'rsrc/css/phui/phui-header-view.css' => '39594ac0', 'rsrc/css/phui/phui-icon.css' => 'd8526aa1', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-list.css' => '43ed2d93', 'rsrc/css/phui/phui-object-box.css' => 'e9f7e938', 'rsrc/css/phui/phui-object-item-list-view.css' => '7ac40b5a', 'rsrc/css/phui/phui-pinboard-view.css' => '3dd4a269', 'rsrc/css/phui/phui-property-list-view.css' => '2f7199e8', 'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '2f562399', 'rsrc/css/phui/phui-tag-view.css' => '1e8aeb04', 'rsrc/css/phui/phui-text.css' => '23e9b4b7', 'rsrc/css/phui/phui-timeline-view.css' => 'bbd990d0', 'rsrc/css/phui/phui-workboard-view.css' => '2bf82d00', 'rsrc/css/phui/phui-workpanel-view.css' => 'ed2a2162', 'rsrc/css/sprite-apps-large.css' => '12ea1ced', 'rsrc/css/sprite-apps.css' => '37ee4f4e', 'rsrc/css/sprite-conpherence.css' => '3b4a0487', 'rsrc/css/sprite-docs.css' => '5f65d0da', 'rsrc/css/sprite-gradient.css' => '4bdb98a7', 'rsrc/css/sprite-login.css' => '878ee4d8', 'rsrc/css/sprite-main-header.css' => '92720ee2', 'rsrc/css/sprite-menu.css' => '28281e16', 'rsrc/css/sprite-minicons.css' => 'df4f76fe', 'rsrc/css/sprite-payments.css' => 'cc085d44', 'rsrc/css/sprite-projects.css' => '7578fa56', 'rsrc/css/sprite-tokens.css' => '1706b943', 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '1cab0752', 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '2ff84fd2', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'a119ee5e', 'rsrc/externals/font/sourcesans/SourceSansPro.woff' => '3614608c', 'rsrc/externals/font/sourcesans/SourceSansProBold.woff' => 'cbf46566', - 'rsrc/externals/javelin/core/Event.js' => '69815cac', - 'rsrc/externals/javelin/core/Stratcom.js' => 'c293f7b9', + 'rsrc/externals/javelin/core/Event.js' => '85ea0626', + 'rsrc/externals/javelin/core/Stratcom.js' => '8b0ad945', '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' => 'da194d4b', 'rsrc/externals/javelin/core/__tests__/util.js' => 'd3b157a9', 'rsrc/externals/javelin/core/init.js' => 'b88ab49e', 'rsrc/externals/javelin/core/init_node.js' => 'd7dde471', - 'rsrc/externals/javelin/core/install.js' => '52a92793', - 'rsrc/externals/javelin/core/util.js' => '65b0b249', - 'rsrc/externals/javelin/docs/Base.js' => '897bb199', - 'rsrc/externals/javelin/docs/onload.js' => '81fb4862', + 'rsrc/externals/javelin/core/install.js' => '1ffb3a9c', + 'rsrc/externals/javelin/core/util.js' => 'a23de73d', + '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' => '77b1cf6f', 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => 'b4c30592', 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed', 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'b6d401d6', 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'e5b406f9', 'rsrc/externals/javelin/ext/view/View.js' => '0f764c35', 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => '0c33c1a0', 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '2fa810fc', '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' => 'bda69c40', 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '5426001c', 'rsrc/externals/javelin/lib/Cookie.js' => '6b3dcf44', - 'rsrc/externals/javelin/lib/DOM.js' => '07d99a3d', + 'rsrc/externals/javelin/lib/DOM.js' => 'c4569c05', 'rsrc/externals/javelin/lib/History.js' => 'c60f4327', - 'rsrc/externals/javelin/lib/JSON.js' => '08e56a4e', - 'rsrc/externals/javelin/lib/Mask.js' => 'b9f26029', - 'rsrc/externals/javelin/lib/Request.js' => '7bad574b', + 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', + 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b', + 'rsrc/externals/javelin/lib/Request.js' => '97258e55', 'rsrc/externals/javelin/lib/Resource.js' => '0f81f8df', 'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692', 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', - 'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862', - 'rsrc/externals/javelin/lib/Vector.js' => 'bd0aedcd', - 'rsrc/externals/javelin/lib/Workflow.js' => '09b15cf1', + 'rsrc/externals/javelin/lib/URI.js' => '6eff08aa', + 'rsrc/externals/javelin/lib/Vector.js' => '23cbb8ac', + 'rsrc/externals/javelin/lib/Workflow.js' => 'd149e002', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074', 'rsrc/externals/javelin/lib/__tests__/URI.js' => '003ed329', 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783', - 'rsrc/externals/javelin/lib/behavior.js' => '8a3ed18b', - 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'e7c21fb3', - 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'c54eeefb', - 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '5f850b5c', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '84f34ab1', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => 'a79b75a4', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '66815d9c', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '62e18640', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => 'cdde23f1', + 'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a', + 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'a5b67173', + 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '61f72a3d', + 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'aa93c7b0', + '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' => '210aa43b', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '316b8fa1', 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', 'rsrc/externals/raphael/raphael.js' => '51ee6b43', 'rsrc/image/BFCFDA.png' => 'd5ec91f4', 'rsrc/image/actions/edit.png' => '2fc41442', 'rsrc/image/apple-touch-icon.png' => '8458dda7', '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/credit_cards.png' => '72b8ede8', '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/fatcow/thumbnails/default.p100.png' => '7d490b01', 'rsrc/image/icon/fatcow/thumbnails/default160x120.png' => 'f2e8a2eb', 'rsrc/image/icon/fatcow/thumbnails/default280x210.png' => '43e8926a', 'rsrc/image/icon/fatcow/thumbnails/default60x45.png' => '0118abed', 'rsrc/image/icon/fatcow/thumbnails/image.p100.png' => 'da23cf97', 'rsrc/image/icon/fatcow/thumbnails/image160x120.png' => '79bb556a', 'rsrc/image/icon/fatcow/thumbnails/image280x210.png' => '91ae054a', 'rsrc/image/icon/fatcow/thumbnails/image60x45.png' => 'c5e1685e', 'rsrc/image/icon/fatcow/thumbnails/pdf.p100.png' => '87d5e065', 'rsrc/image/icon/fatcow/thumbnails/pdf160x120.png' => 'ac9edbf5', 'rsrc/image/icon/fatcow/thumbnails/pdf280x210.png' => '1c585653', 'rsrc/image/icon/fatcow/thumbnails/pdf60x45.png' => 'c0db4143', 'rsrc/image/icon/fatcow/thumbnails/zip.p100.png' => '6ea5aae4', 'rsrc/image/icon/fatcow/thumbnails/zip160x120.png' => '75f9cd0f', 'rsrc/image/icon/fatcow/thumbnails/zip280x210.png' => 'dfda5b8e', 'rsrc/image/icon/fatcow/thumbnails/zip60x45.png' => 'af11bf3e', '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/loading.gif' => '75d384cc', 'rsrc/image/loading/boating_24.gif' => '5c90f086', 'rsrc/image/loading/compass_24.gif' => 'b36b4f46', 'rsrc/image/loading/loading_24.gif' => '26bc9adc', 'rsrc/image/loading/loading_48.gif' => '6a4994c7', 'rsrc/image/loading/loading_d48.gif' => 'cdcbe900', 'rsrc/image/loading/loading_w24.gif' => '7662fa2b', '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/search-white.png' => '64cc0d45', 'rsrc/image/search.png' => '82625a7e', 'rsrc/image/sprite-apps-X2.png' => '10fe124a', 'rsrc/image/sprite-apps-X4.png' => '9c151271', 'rsrc/image/sprite-apps-large-X2.png' => '4a828a0f', 'rsrc/image/sprite-apps-large.png' => '141d8c93', 'rsrc/image/sprite-apps-xlarge.png' => 'a751a580', 'rsrc/image/sprite-apps.png' => 'f6a0599f', 'rsrc/image/sprite-conpherence-X2.png' => 'cd2d08d7', 'rsrc/image/sprite-conpherence.png' => 'a5ab2eb7', 'rsrc/image/sprite-docs-X2.png' => '6dc1adad', 'rsrc/image/sprite-docs.png' => '4636297f', 'rsrc/image/sprite-gradient.png' => 'ec15a417', 'rsrc/image/sprite-login-X2.png' => '3b2182c4', 'rsrc/image/sprite-login.png' => '9effdc71', 'rsrc/image/sprite-main-header.png' => '83521873', 'rsrc/image/sprite-menu-X2.png' => '39d78f97', 'rsrc/image/sprite-menu.png' => '259dab45', 'rsrc/image/sprite-minicons-X2.png' => '55377e4e', 'rsrc/image/sprite-minicons.png' => '272644ea', 'rsrc/image/sprite-payments.png' => 'd8576309', 'rsrc/image/sprite-projects-X2.png' => '218fdc8b', 'rsrc/image/sprite-projects.png' => '631ff9a7', 'rsrc/image/sprite-tokens-X2.png' => 'b4776580', 'rsrc/image/sprite-tokens.png' => '25b75533', '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' => '4a07e8e3', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'f51afce0', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'a826c925', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '58f7803f', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de', 'rsrc/js/application/conpherence/behavior-menu.js' => 'f0a41b9f', 'rsrc/js/application/conpherence/behavior-pontificate.js' => '85ab3c8e', 'rsrc/js/application/conpherence/behavior-widget-pane.js' => '40b1ff90', 'rsrc/js/application/countdown/timer.js' => '361e3ed3', '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' => '880fa5ac', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 'rsrc/js/application/differential/ChangesetViewManager.js' => 'd2907473', 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746', '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' => '127f2018', 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '710f209e', 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799', 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '8d199d97', 'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0', 'rsrc/js/application/differential/behavior-show-all-comments.js' => '7c273581', 'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf', 'rsrc/js/application/differential/behavior-show-more.js' => 'dd7e8ef5', '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' => 'f7f1289f', 'rsrc/js/application/diffusion/behavior-jump-to.js' => '9db3d160', '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/harbormaster/behavior-reorder-steps.js' => 'b716477f', 'rsrc/js/application/herald/HeraldRuleEditor.js' => '58e048fc', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', 'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'f588412e', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '7b98d7c5', 'rsrc/js/application/maniphest/behavior-line-chart.js' => '22e16ae7', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2', 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '84845b5b', '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' => 'f8248bc5', 'rsrc/js/application/owners/OwnersPathEditor.js' => 'aa1733d0', 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3', 'rsrc/js/application/passphrase/phame-credential-control.js' => '3d51a746', 'rsrc/js/application/phame/phame-post-preview.js' => 'be807912', 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '9c2623f4', 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => '152178f0', 'rsrc/js/application/phortune/behavior-balanced-payment-form.js' => '3b3e1664', 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '1693a296', 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'ab8d2723', 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'f3fef818', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '92918fcb', 'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b', 'rsrc/js/application/projects/behavior-boards-dropdown.js' => '0ec56e1d', 'rsrc/js/application/projects/behavior-project-boards.js' => '1cb113dc', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'ab836011', - 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'cd9e7094', + 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', 'rsrc/js/application/repository/repository-crossreference.js' => 'f9539603', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => 'd6f54db0', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9f7309fb', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '71f66c08', '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' => '7a9677fc', 'rsrc/js/core/Busy.js' => '6453c869', 'rsrc/js/core/DragAndDropFileUpload.js' => '1d8ad5c3', 'rsrc/js/core/DraggableList.js' => '2cad29d1', 'rsrc/js/core/FileUpload.js' => 'a4ae61bf', 'rsrc/js/core/Hovercard.js' => '7e8468ae', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'ad7a69ca', 'rsrc/js/core/MultirowRowManager.js' => '41e47dea', 'rsrc/js/core/Notification.js' => '0c6946e7', 'rsrc/js/core/Prefab.js' => '41ed7994', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => 'b3ec3cfc', 'rsrc/js/core/ToolTip.js' => '3915d490', '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' => '357b6e9b', 'rsrc/js/core/behavior-device.js' => '03d6ed07', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '92eb531d', 'rsrc/js/core/behavior-error-log.js' => 'a5d7cf86', 'rsrc/js/core/behavior-fancy-datepicker.js' => 'a5573bcd', 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '3b1557b3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', 'rsrc/js/core/behavior-global-drag-and-drop.js' => '3672899b', 'rsrc/js/core/behavior-high-security-warning.js' => '8fc1c918', '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-konami.js' => '5bc2cb21', 'rsrc/js/core/behavior-lightbox-attachments.js' => '0720f2cf', 'rsrc/js/core/behavior-line-linker.js' => 'f726d506', 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '39841ead', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '14d7a8b8', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'e32d14ab', '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-search-typeahead.js' => '5a376f34', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', - 'rsrc/js/core/behavior-toggle-class.js' => 'a82a7769', + 'rsrc/js/core/behavior-toggle-class.js' => 'e566f52c', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => '3ee3408b', 'rsrc/js/core/behavior-watch-anchor.js' => '06e05112', 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => 'a3e2244e', 'rsrc/js/phui/behavior-phui-timeline-dropdown-menu.js' => '4d94d9c3', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '6e8cefa4', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', 'rsrc/swf/aphlict.swf' => 'f19daffb', ), 'symbols' => array( 'aphront-bars' => '231ac33c', 'aphront-contextbar-view-css' => '1c3b0529', 'aphront-dark-console-css' => '6378ef3d', 'aphront-dialog-view-css' => '4dbbe3bb', 'aphront-error-view-css' => '3462dbee', 'aphront-list-filter-view-css' => '2ae43867', 'aphront-multi-column-view-css' => '1b95ab2e', 'aphront-pager-view-css' => '2e3539af', 'aphront-panel-view-css' => '5846dfa2', 'aphront-request-failure-view-css' => 'da14df31', 'aphront-table-view-css' => 'b22b7216', 'aphront-tokenizer-control-css' => '82ce2142', 'aphront-tooltip-css' => '9c90229d', 'aphront-two-column-view-css' => '16ab3ad2', 'aphront-typeahead-control-css' => 'a989b5b3', 'auth-css' => '1e655982', 'changeset-view-manager' => 'd2907473', 'config-options-css' => '7fedf08b', 'config-welcome-css' => 'b0d16200', 'conpherence-menu-css' => 'e1e0fdf1', 'conpherence-message-pane-css' => '11a393ca', 'conpherence-notification-css' => '04a6e10a', 'conpherence-update-css' => '1099a660', 'conpherence-widget-pane-css' => 'bf275a6c', 'differential-changeset-view-css' => 'ff8eacf8', 'differential-core-view-css' => '7ac3cabc', 'differential-inline-comment-editor' => 'f2441746', 'differential-results-table-css' => '239924f9', 'differential-revision-add-comment-css' => 'c478bcaa', 'differential-revision-comment-css' => '48186045', 'differential-revision-history-css' => '0e8eb855', 'differential-revision-list-css' => 'f3c47d33', 'differential-table-of-contents-css' => '6bf8e1d2', 'diffusion-commit-view-css' => '92d1e8f9', 'diffusion-icons-css' => '9c5828da', 'diffusion-source-css' => '66fdf661', 'diviner-shared-css' => '38813222', 'font-fontawesome' => '73d075c3', 'font-source-sans-pro' => '91d53463', 'global-drag-and-drop-css' => '697324ad', 'harbormaster-css' => 'cec833b7', 'herald-css' => 'c544dd1c', 'herald-rule-editor' => '58e048fc', 'herald-test-css' => '778b008e', 'inline-comment-summary-css' => '8cfd34e8', 'javelin-aphlict' => '4a07e8e3', - 'javelin-behavior' => '8a3ed18b', + 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => 'f51afce0', 'javelin-behavior-aphlict-listen' => 'a826c925', 'javelin-behavior-aphlict-status' => '58f7803f', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-crop' => 'fa0f4fc2', 'javelin-behavior-aphront-drag-and-drop-textarea' => '92eb531d', 'javelin-behavior-aphront-form-disable-on-submit' => '3b1557b3', 'javelin-behavior-aphront-more' => 'a80d0378', 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', 'javelin-behavior-balanced-payment-form' => '3b3e1664', 'javelin-behavior-boards-dropdown' => '0ec56e1d', 'javelin-behavior-choose-control' => '6153c708', 'javelin-behavior-config-reorder-fields' => '14a827de', 'javelin-behavior-conpherence-menu' => 'f0a41b9f', 'javelin-behavior-conpherence-pontificate' => '85ab3c8e', 'javelin-behavior-conpherence-widget-pane' => '40b1ff90', 'javelin-behavior-countdown-timer' => '361e3ed3', 'javelin-behavior-dark-console' => '357b6e9b', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '82439934', 'javelin-behavior-dashboard-query-panel-select' => '880fa5ac', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-device' => '03d6ed07', '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' => '710f209e', 'javelin-behavior-differential-edit-inline-comments' => '00861799', 'javelin-behavior-differential-feedback-preview' => '127f2018', 'javelin-behavior-differential-keyboard-navigation' => '8d199d97', 'javelin-behavior-differential-populate' => 'bdb3e4d0', 'javelin-behavior-differential-show-field-details' => 'bba9eedf', 'javelin-behavior-differential-show-more' => 'dd7e8ef5', 'javelin-behavior-differential-toggle-files' => 'ca3f91eb', 'javelin-behavior-differential-user-select' => 'a8d8459d', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => 'f7f1289f', 'javelin-behavior-diffusion-jump-to' => '9db3d160', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => '2b228192', 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-error-log' => 'a5d7cf86', 'javelin-behavior-fancy-datepicker' => 'a5573bcd', 'javelin-behavior-global-drag-and-drop' => '3672899b', 'javelin-behavior-harbormaster-reorder-steps' => 'b716477f', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => '8fc1c918', 'javelin-behavior-history-install' => '7ee2b591', 'javelin-behavior-icon-composer' => '8ef9ab58', 'javelin-behavior-konami' => '5bc2cb21', 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => '0720f2cf', 'javelin-behavior-line-chart' => '22e16ae7', 'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-maniphest-batch-editor' => 'f588412e', 'javelin-behavior-maniphest-batch-selector' => '7b98d7c5', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '84845b5b', 'javelin-behavior-maniphest-transaction-controls' => '44168bad', 'javelin-behavior-maniphest-transaction-expand' => '5fefb143', 'javelin-behavior-maniphest-transaction-preview' => 'f8248bc5', 'javelin-behavior-owners-path-editor' => '7a68dda3', 'javelin-behavior-passphrase-credential-control' => '3d51a746', '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' => 'f726d506', 'javelin-behavior-phabricator-nav' => '14d7a8b8', 'javelin-behavior-phabricator-notification-example' => '7a9677fc', 'javelin-behavior-phabricator-object-selector' => '39841ead', 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => 'e32d14ab', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', 'javelin-behavior-phabricator-search-typeahead' => '5a376f34', 'javelin-behavior-phabricator-show-all-transactions' => '7c273581', 'javelin-behavior-phabricator-tooltips' => '3ee3408b', 'javelin-behavior-phabricator-transaction-comment-form' => '9f7309fb', 'javelin-behavior-phabricator-transaction-list' => '71f66c08', 'javelin-behavior-phabricator-watch-anchor' => '06e05112', 'javelin-behavior-phame-post-preview' => 'be807912', 'javelin-behavior-pholio-mock-edit' => '9c2623f4', 'javelin-behavior-pholio-mock-view' => '152178f0', 'javelin-behavior-phui-object-box-tabs' => 'a3e2244e', 'javelin-behavior-phui-timeline-dropdown-menu' => '4d94d9c3', 'javelin-behavior-policy-control' => 'f3fef818', 'javelin-behavior-policy-rule-editor' => '92918fcb', 'javelin-behavior-ponder-votebox' => '4e9b766b', 'javelin-behavior-project-boards' => '1cb113dc', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-refresh-csrf' => '7814b593', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 'javelin-behavior-releeph-request-state-change' => 'ab836011', - 'javelin-behavior-releeph-request-typeahead' => 'cd9e7094', + 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 'javelin-behavior-remarkup-preview' => 'f7379f45', 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-repository-crossreference' => 'f9539603', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-on-click' => '4e3e79a6', 'javelin-behavior-slowvote-embed' => 'd6f54db0', 'javelin-behavior-stripe-payment-form' => '1693a296', 'javelin-behavior-test-payment-form' => 'ab8d2723', - 'javelin-behavior-toggle-class' => 'a82a7769', + 'javelin-behavior-toggle-class' => 'e566f52c', 'javelin-behavior-view-placeholder' => '2fa810fc', 'javelin-behavior-workflow' => '0a3f3021', 'javelin-color' => '7e41274a', 'javelin-cookie' => '6b3dcf44', 'javelin-diffusion-locate-file-source' => 'b42eddc7', - 'javelin-dom' => '07d99a3d', + 'javelin-dom' => 'c4569c05', 'javelin-dynval' => 'f6555212', - 'javelin-event' => '69815cac', + 'javelin-event' => '85ea0626', 'javelin-fx' => '54b612ba', 'javelin-history' => 'c60f4327', - 'javelin-install' => '52a92793', - 'javelin-json' => '08e56a4e', + 'javelin-install' => '1ffb3a9c', + 'javelin-json' => '69adf288', 'javelin-magical-init' => 'b88ab49e', - 'javelin-mask' => 'b9f26029', + 'javelin-mask' => '8a41885b', 'javelin-reactor' => '77b1cf6f', 'javelin-reactor-dom' => 'b6d401d6', 'javelin-reactor-node-calmer' => '76f4ebed', 'javelin-reactornode' => 'b4c30592', - 'javelin-request' => '7bad574b', + 'javelin-request' => '97258e55', 'javelin-resource' => '0f81f8df', 'javelin-routable' => 'b3e7d692', 'javelin-router' => '29274e2b', - 'javelin-stratcom' => 'c293f7b9', - 'javelin-tokenizer' => 'e7c21fb3', - 'javelin-typeahead' => 'c54eeefb', - 'javelin-typeahead-composite-source' => '84f34ab1', - 'javelin-typeahead-normalizer' => '5f850b5c', - 'javelin-typeahead-ondemand-source' => 'a79b75a4', - 'javelin-typeahead-preloaded-source' => '66815d9c', - 'javelin-typeahead-source' => '62e18640', - 'javelin-typeahead-static-source' => 'cdde23f1', - 'javelin-uri' => 'd9a9b862', - 'javelin-util' => '65b0b249', - 'javelin-vector' => 'bd0aedcd', + 'javelin-stratcom' => '8b0ad945', + 'javelin-tokenizer' => 'a5b67173', + 'javelin-typeahead' => '61f72a3d', + 'javelin-typeahead-composite-source' => '503e17fd', + 'javelin-typeahead-normalizer' => 'aa93c7b0', + 'javelin-typeahead-ondemand-source' => '8b3fd187', + 'javelin-typeahead-preloaded-source' => '54f314a0', + 'javelin-typeahead-source' => '210aa43b', + 'javelin-typeahead-static-source' => '316b8fa1', + 'javelin-uri' => '6eff08aa', + 'javelin-util' => 'a23de73d', + 'javelin-vector' => '23cbb8ac', 'javelin-view' => '0f764c35', 'javelin-view-html' => 'e5b406f9', 'javelin-view-interpreter' => '0c33c1a0', 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', - 'javelin-workflow' => '09b15cf1', + 'javelin-workflow' => 'd149e002', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => '8f380ebc', 'maniphest-report-css' => '6fc16517', 'maniphest-task-edit-css' => '8e23031b', 'maniphest-task-summary-css' => '00c3be7a', 'multirow-row-manager' => '41e47dea', 'owners-path-editor' => 'aa1733d0', 'owners-path-editor-css' => '2f00933b', 'paste-css' => 'aa1767d1', 'path-typeahead' => 'f7fc67ec', 'people-profile-css' => 'ba7b2762', 'phabricator-action-list-view-css' => '9ee9910a', 'phabricator-application-launch-view-css' => '8b7e271d', 'phabricator-busy' => '6453c869', 'phabricator-chatlog-css' => '852140ff', 'phabricator-content-source-view-css' => '4b8b05d4', 'phabricator-core-css' => '40151074', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => '7fbf25b8', 'phabricator-dashboard-css' => 'a2bfdcbf', 'phabricator-drag-and-drop-file-upload' => '1d8ad5c3', 'phabricator-draggable-list' => '2cad29d1', 'phabricator-fatal-config-template-css' => '25d446d6', 'phabricator-feed-css' => '4e544db4', 'phabricator-file-upload' => 'a4ae61bf', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', 'phabricator-hovercard' => '7e8468ae', 'phabricator-hovercard-view-css' => '893f4783', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'ad7a69ca', 'phabricator-main-menu-view' => 'aceca0e9', 'phabricator-nav-view-css' => '9283c2df', 'phabricator-notification' => '0c6946e7', 'phabricator-notification-css' => 'ef2c9b34', 'phabricator-notification-menu-css' => '8ae4a008', 'phabricator-object-selector-css' => '029a133d', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '41ed7994', 'phabricator-profile-css' => 'b459416e', 'phabricator-remarkup-css' => 'ad4c0676', 'phabricator-search-results-css' => 'f240504c', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-side-menu-view-css' => 'a2ccd7bd', 'phabricator-slowvote-css' => '266df6a1', 'phabricator-source-code-view-css' => '7d346aa4', 'phabricator-standard-page-view' => '517cdfb1', 'phabricator-textareautils' => 'b3ec3cfc', 'phabricator-tooltip' => '3915d490', 'phabricator-transaction-view-css' => '5d0cae25', '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' => 'd1c137f2', 'phame-css' => '19ecc703', 'pholio-css' => '47dffb9c', 'pholio-edit-css' => '3ad9d1ee', 'pholio-inline-comments-css' => '8e545e49', 'phortune-credit-card-form' => '2290aeef', 'phortune-credit-card-form-css' => 'b25b4beb', 'phrequent-css' => 'ffc185ad', 'phriction-document-css' => '7d7f0071', 'phui-action-header-view-css' => '83e2cc86', 'phui-box-css' => '7b3a2eed', 'phui-button-css' => 'c7412aa1', 'phui-calendar-css' => '5e1ad989', 'phui-calendar-day-css' => 'de035c8a', 'phui-calendar-list-css' => 'c1d0ca59', 'phui-calendar-month-css' => 'a92e47d2', 'phui-document-view-css' => 'a5615198', 'phui-feed-story-css' => 'e2c9bc83', 'phui-font-icon-base-css' => 'eb84f033', 'phui-fontkit-css' => 'abeb59f0', 'phui-form-css' => 'b78ec020', 'phui-form-view-css' => 'ebac1b1d', 'phui-header-view-css' => '39594ac0', 'phui-icon-view-css' => 'd8526aa1', 'phui-image-mask-css' => '5a8b09c8', 'phui-info-panel-css' => '27ea50a1', 'phui-list-view-css' => '43ed2d93', 'phui-object-box-css' => 'e9f7e938', 'phui-object-item-list-view-css' => '7ac40b5a', 'phui-pinboard-view-css' => '3dd4a269', 'phui-property-list-view-css' => '2f7199e8', 'phui-remarkup-preview-css' => '19ad512b', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '2f562399', 'phui-tag-view-css' => '1e8aeb04', 'phui-text-css' => '23e9b4b7', 'phui-timeline-view-css' => 'bbd990d0', 'phui-workboard-view-css' => '2bf82d00', 'phui-workpanel-view-css' => 'ed2a2162', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '6e8cefa4', 'phuix-dropdown-menu' => 'bd4c8dca', 'policy-css' => '957ea14c', 'policy-edit-css' => '05cca26a', 'policy-transaction-detail-css' => '82100a43', 'ponder-comment-table-css' => '6cdccea7', 'ponder-feed-view-css' => 'e62615b6', 'ponder-post-css' => 'ebab8a70', 'ponder-vote-css' => '8ed6ed8b', 'project-icon-css' => 'c2ecb7f1', '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' => '69e640e7', 'sprite-apps-css' => '37ee4f4e', 'sprite-apps-large-css' => '12ea1ced', 'sprite-conpherence-css' => '3b4a0487', 'sprite-docs-css' => '5f65d0da', 'sprite-gradient-css' => '4bdb98a7', 'sprite-login-css' => '878ee4d8', 'sprite-main-header-css' => '92720ee2', 'sprite-menu-css' => '28281e16', 'sprite-minicons-css' => 'df4f76fe', 'sprite-payments-css' => 'cc085d44', 'sprite-projects-css' => '7578fa56', 'sprite-tokens-css' => '1706b943', 'syntax-highlighting-css' => '3c18c1cb', 'tokens-css' => '3d0f239e', ), 'requires' => array( '00861799' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-util', 4 => 'javelin-vector', 5 => 'differential-inline-comment-editor', ), '029a133d' => array( 0 => 'aphront-dialog-view-css', ), '03d6ed07' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-vector', 4 => 'javelin-install', ), '065227cc' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', ), '06e05112' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-vector', ), '0720f2cf' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-mask', 4 => 'javelin-util', 5 => 'phabricator-busy', ), - '07d99a3d' => - array( - 0 => 'javelin-magical-init', - 1 => 'javelin-install', - 2 => 'javelin-util', - 3 => 'javelin-vector', - 4 => 'javelin-stratcom', - ), - '08e56a4e' => - array( - 0 => 'javelin-install', - ), - '09b15cf1' => - array( - 0 => 'javelin-stratcom', - 1 => 'javelin-request', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - 4 => 'javelin-install', - 5 => 'javelin-util', - 6 => 'javelin-mask', - 7 => 'javelin-uri', - 8 => 'javelin-routable', - ), '0a3f3021' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-workflow', 3 => 'javelin-dom', 4 => 'javelin-router', ), '0c33c1a0' => array( 0 => 'javelin-view', 1 => 'javelin-install', 2 => 'javelin-dom', ), '0c6946e7' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-util', 4 => 'phabricator-notification-css', ), '0ec56e1d' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'phuix-dropdown-menu', ), '0f764c35' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), '0f81f8df' => array( 0 => 'javelin-util', 1 => 'javelin-uri', 2 => 'javelin-install', ), '127f2018' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-request', 4 => 'javelin-util', 5 => 'phabricator-shaped-request', ), '14a827de' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-json', 4 => 'phabricator-draggable-list', ), '14d7a8b8' => array( 0 => 'javelin-behavior', 1 => 'javelin-behavior-device', 2 => 'javelin-stratcom', 3 => 'javelin-dom', 4 => 'javelin-magical-init', 5 => 'javelin-vector', 6 => 'javelin-request', 7 => 'javelin-util', ), '152178f0' => array( 0 => 'javelin-behavior', 1 => 'javelin-util', 2 => 'javelin-stratcom', 3 => 'javelin-dom', 4 => 'javelin-vector', 5 => 'javelin-magical-init', 6 => 'javelin-request', 7 => 'javelin-history', 8 => 'javelin-workflow', 9 => 'javelin-mask', 10 => 'javelin-behavior-device', 11 => 'phabricator-keyboard-shortcut', ), '1693a296' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'phortune-credit-card-form', ), '1ae869f2' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'phabricator-keyboard-shortcut-manager', ), '1cb113dc' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-stratcom', 4 => 'javelin-workflow', 5 => 'phabricator-draggable-list', ), '1d8ad5c3' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'javelin-request', 3 => 'javelin-dom', 4 => 'javelin-uri', 5 => 'phabricator-file-upload', ), '1def2711' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), + '1ffb3a9c' => + array( + 0 => 'javelin-util', + 1 => 'javelin-magical-init', + ), + '210aa43b' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-typeahead-normalizer', + ), '2290aeef' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-json', 3 => 'javelin-workflow', 4 => 'javelin-util', ), '22e16ae7' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-vector', ), + '23cbb8ac' => + array( + 0 => 'javelin-install', + 1 => 'javelin-event', + ), '2926fff2' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', ), '29274e2b' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), '2b228192' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-workflow', 4 => 'javelin-json', ), '2cad29d1' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-util', 4 => 'javelin-vector', 5 => 'javelin-magical-init', ), '2fa810fc' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-view-renderer', 3 => 'javelin-install', ), + '316b8fa1' => + array( + 0 => 'javelin-install', + 1 => 'javelin-typeahead-source', + ), '357b6e9b' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-util', 3 => 'javelin-dom', 4 => 'javelin-request', 5 => 'phabricator-keyboard-shortcut', ), '361e3ed3' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', ), '3672899b' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-uri', 3 => 'javelin-mask', 4 => 'phabricator-drag-and-drop-file-upload', ), '3915d490' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'javelin-dom', 3 => 'javelin-vector', ), '39841ead' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-request', 3 => 'javelin-util', ), '3ab51e2c' => array( 0 => 'javelin-behavior', 1 => 'javelin-behavior-device', 2 => 'javelin-stratcom', 3 => 'javelin-vector', 4 => 'javelin-dom', 5 => 'javelin-magical-init', ), '3b1557b3' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), '3b3e1664' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'phortune-credit-card-form', ), '3d51a746' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', 4 => 'javelin-util', 5 => 'javelin-uri', ), '3ee3408b' => array( 0 => 'javelin-behavior', 1 => 'javelin-behavior-device', 2 => 'javelin-stratcom', 3 => 'phabricator-tooltip', ), '40a6a403' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), '40b1ff90' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', 4 => 'javelin-util', 5 => 'phabricator-notification', 6 => 'javelin-behavior-device', 7 => 'phuix-dropdown-menu', 8 => 'phuix-action-list-view', 9 => 'phuix-action-view', ), '41e47dea' => array( 0 => 'javelin-install', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-util', ), '41ed7994' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'javelin-dom', 3 => 'javelin-typeahead', 4 => 'javelin-tokenizer', 5 => 'javelin-typeahead-preloaded-source', 6 => 'javelin-typeahead-ondemand-source', 7 => 'javelin-dom', 8 => 'javelin-stratcom', 9 => 'javelin-util', ), '44168bad' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'phabricator-prefab', ), '469c0d9e' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-workflow', ), '47c794d8' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), '4a07e8e3' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), '4d94d9c3' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'phuix-dropdown-menu', ), '4e3e79a6' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), '4e9b766b' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-stratcom', 4 => 'javelin-request', ), '4fdb476d' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), + '503e17fd' => + array( + 0 => 'javelin-install', + 1 => 'javelin-typeahead-source', + 2 => 'javelin-util', + ), '519705ea' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), - '52a92793' => - array( - 0 => 'javelin-util', - 1 => 'javelin-magical-init', - ), '54b612ba' => array( 0 => 'javelin-color', 1 => 'javelin-install', 2 => 'javelin-util', ), + '54f314a0' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-request', + 3 => 'javelin-typeahead-source', + ), '558829c2' => array( 0 => 'javelin-stratcom', 1 => 'javelin-behavior', 2 => 'javelin-vector', 3 => 'javelin-dom', ), '58e048fc' => array( 0 => 'multirow-row-manager', 1 => 'javelin-install', 2 => 'javelin-util', 3 => 'javelin-dom', 4 => 'javelin-stratcom', 5 => 'javelin-json', 6 => 'phabricator-prefab', ), '58f7803f' => array( 0 => 'javelin-behavior', 1 => 'javelin-aphlict', 2 => 'phabricator-phtize', 3 => 'javelin-dom', ), '59b251eb' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-vector', 3 => 'javelin-dom', ), '5a376f34' => array( 0 => 'javelin-behavior', 1 => 'javelin-typeahead-ondemand-source', 2 => 'javelin-typeahead', 3 => 'javelin-dom', 4 => 'javelin-uri', 5 => 'javelin-util', 6 => 'javelin-stratcom', ), '5bc2cb21' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', ), - '5f850b5c' => - array( - 0 => 'javelin-install', - ), '5fefb143' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-workflow', 3 => 'javelin-stratcom', ), '60821bc7' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), '6153c708' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-workflow', ), - '62e18640' => + '61cbc29a' => array( - 0 => 'javelin-install', + 0 => 'javelin-magical-init', 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-typeahead-normalizer', ), - '6453c869' => + '61f72a3d' => array( 0 => 'javelin-install', 1 => 'javelin-dom', - 2 => 'javelin-fx', + 2 => 'javelin-vector', + 3 => 'javelin-util', ), - '66815d9c' => + '6453c869' => array( 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-request', - 3 => 'javelin-typeahead-source', + 1 => 'javelin-dom', + 2 => 'javelin-fx', ), - '69815cac' => + '69adf288' => array( 0 => 'javelin-install', ), '6b3dcf44' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), '6c2b09a2' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), '6d3e1947' => array( 0 => 'javelin-behavior', 1 => 'javelin-diffusion-locate-file-source', 2 => 'javelin-dom', 3 => 'javelin-typeahead', 4 => 'javelin-uri', ), '6e8cefa4' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-util', ), + '6eff08aa' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + ), '710f209e' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-stratcom', 4 => 'javelin-workflow', 5 => 'phuix-dropdown-menu', 6 => 'phuix-action-list-view', 7 => 'phuix-action-view', 8 => 'phabricator-phtize', 9 => 'changeset-view-manager', ), '71f66c08' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-workflow', 3 => 'javelin-dom', 4 => 'javelin-uri', 5 => 'phabricator-textareautils', ), '7319e029' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', ), '76b9fc3e' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-workflow', 3 => 'javelin-dom', 4 => 'phabricator-draggable-list', ), '76f4ebed' => array( 0 => 'javelin-install', 1 => 'javelin-reactor', 2 => 'javelin-util', ), '77b1cf6f' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), '7814b593' => array( 0 => 'javelin-request', 1 => 'javelin-behavior', 2 => 'javelin-dom', 3 => 'javelin-router', 4 => 'javelin-util', 5 => 'phabricator-busy', ), '7a68dda3' => array( 0 => 'owners-path-editor', 1 => 'javelin-behavior', ), '7a9677fc' => array( 0 => 'phabricator-notification', 1 => 'javelin-stratcom', 2 => 'javelin-behavior', ), '7b98d7c5' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-util', ), - '7bad574b' => - array( - 0 => 'javelin-install', - 1 => 'javelin-stratcom', - 2 => 'javelin-util', - 3 => 'javelin-behavior', - 4 => 'javelin-json', - 5 => 'javelin-dom', - 6 => 'javelin-resource', - 7 => 'javelin-routable', - ), '7c273581' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), '7cbe244b' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'javelin-request', 3 => 'javelin-router', ), '7e41274a' => array( 0 => 'javelin-install', ), '7e8468ae' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-vector', 3 => 'javelin-request', 4 => 'javelin-uri', ), '7ebaeed3' => array( 0 => 'herald-rule-editor', 1 => 'javelin-behavior', ), '7ee2b591' => array( 0 => 'javelin-behavior', 1 => 'javelin-history', ), '82ce2142' => array( 0 => 'aphront-typeahead-control-css', ), '84845b5b' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', 4 => 'phabricator-draggable-list', ), - '84f34ab1' => - array( - 0 => 'javelin-install', - 1 => 'javelin-typeahead-source', - 2 => 'javelin-util', - ), '85ab3c8e' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-workflow', 4 => 'javelin-stratcom', ), + '85ea0626' => + array( + 0 => 'javelin-install', + ), '880fa5ac' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', ), '88236f00' => array( 0 => 'javelin-behavior', 1 => 'phabricator-keyboard-shortcut', 2 => 'javelin-stratcom', ), '886fd850' => array( 0 => 'javelin-install', 1 => 'javelin-reactor-dom', 2 => 'javelin-view-html', 3 => 'javelin-view-interpreter', 4 => 'javelin-view-renderer', ), - '8a3ed18b' => + '8a41885b' => array( - 0 => 'javelin-magical-init', + 0 => 'javelin-install', + 1 => 'javelin-dom', + ), + '8b0ad945' => + array( + 0 => 'javelin-install', + 1 => 'javelin-event', + 2 => 'javelin-util', + 3 => 'javelin-magical-init', + ), + '8b3fd187' => + array( + 0 => 'javelin-install', 1 => 'javelin-util', + 2 => 'javelin-request', + 3 => 'javelin-typeahead-source', ), '8d199d97' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'phabricator-keyboard-shortcut', ), '8ef9ab58' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', ), '8fc1c918' => array( 0 => 'javelin-behavior', 1 => 'javelin-uri', 2 => 'phabricator-notification', ), '92918fcb' => array( 0 => 'javelin-behavior', 1 => 'multirow-row-manager', 2 => 'javelin-dom', 3 => 'javelin-util', 4 => 'phabricator-prefab', 5 => 'javelin-tokenizer', 6 => 'javelin-typeahead', 7 => 'javelin-typeahead-preloaded-source', 8 => 'javelin-json', ), '92eb531d' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'phabricator-drag-and-drop-file-upload', 3 => 'phabricator-textareautils', ), '9414ff18' => array( 0 => 'javelin-behavior', 1 => 'javelin-resource', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', 4 => 'javelin-util', ), + '97258e55' => + array( + 0 => 'javelin-install', + 1 => 'javelin-stratcom', + 2 => 'javelin-util', + 3 => 'javelin-behavior', + 4 => 'javelin-json', + 5 => 'javelin-dom', + 6 => 'javelin-resource', + 7 => 'javelin-routable', + ), '988040b4' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), '9c2623f4' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-workflow', 4 => 'phabricator-phtize', 5 => 'phabricator-drag-and-drop-file-upload', 6 => 'phabricator-draggable-list', ), '9db3d160' => array( 0 => 'javelin-behavior', 1 => 'javelin-vector', 2 => 'javelin-dom', ), '9f7309fb' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-request', 4 => 'phabricator-shaped-request', ), 'a155550f' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), 'a3e2244e' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), 'a4ae61bf' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'phabricator-notification', ), 'a5573bcd' => array( 0 => 'javelin-behavior', 1 => 'javelin-util', 2 => 'javelin-dom', 3 => 'javelin-stratcom', 4 => 'javelin-vector', ), - 'a5d7cf86' => + 'a5b67173' => array( 0 => 'javelin-dom', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + 3 => 'javelin-install', ), - 'a79b75a4' => + 'a5d7cf86' => array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-request', - 3 => 'javelin-typeahead-source', + 0 => 'javelin-dom', ), 'a80d0378' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), 'a826c925' => array( 0 => 'javelin-behavior', 1 => 'javelin-aphlict', 2 => 'javelin-stratcom', 3 => 'javelin-request', 4 => 'javelin-uri', 5 => 'javelin-dom', 6 => 'javelin-json', 7 => 'javelin-router', 8 => 'javelin-util', 9 => 'phabricator-notification', ), - 'a82a7769' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - ), 'a8d8459d' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', ), 'a8da01f0' => array( 0 => 'javelin-behavior', 1 => 'javelin-uri', 2 => 'phabricator-keyboard-shortcut', ), 'a9f88de2' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', 4 => 'javelin-fx', 5 => 'javelin-util', ), 'aa1733d0' => array( 0 => 'multirow-row-manager', 1 => 'javelin-install', 2 => 'path-typeahead', 3 => 'javelin-dom', 4 => 'javelin-util', 5 => 'phabricator-prefab', ), + 'aa93c7b0' => + array( + 0 => 'javelin-install', + ), 'ab836011' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-workflow', 4 => 'javelin-util', 5 => 'phabricator-keyboard-shortcut', ), 'ab8d2723' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'phortune-credit-card-form', ), 'ad7a69ca' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'javelin-stratcom', 3 => 'javelin-dom', 4 => 'javelin-vector', ), 'b1f0ccee' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), 'b2b4fbaf' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-uri', 3 => 'javelin-request', ), 'b3a4b884' => array( 0 => 'javelin-behavior', 1 => 'phabricator-prefab', ), 'b3e7d692' => array( 0 => 'javelin-install', ), 'b3ec3cfc' => array( 0 => 'javelin-install', ), 'b42eddc7' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-typeahead-preloaded-source', 3 => 'javelin-util', ), 'b4c30592' => array( 0 => 'javelin-install', 1 => 'javelin-reactor', 2 => 'javelin-util', 3 => 'javelin-reactor-node-calmer', ), 'b5c256b8' => array( 0 => 'javelin-install', 1 => 'javelin-dom', ), 'b6d401d6' => array( 0 => 'javelin-dom', 1 => 'javelin-dynval', 2 => 'javelin-reactor', 3 => 'javelin-reactornode', 4 => 'javelin-install', 5 => 'javelin-util', ), 'b716477f' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-workflow', 3 => 'javelin-dom', 4 => 'phabricator-draggable-list', ), - 'b9f26029' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - ), 'bba9eedf' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), - 'bd0aedcd' => - array( - 0 => 'javelin-install', - 1 => 'javelin-event', - ), 'bd4c8dca' => array( 0 => 'javelin-install', 1 => 'javelin-util', 2 => 'javelin-dom', 3 => 'javelin-vector', 4 => 'javelin-stratcom', ), 'bdaf4d04' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-request', ), 'bdb3e4d0' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'phabricator-tooltip', 4 => 'changeset-view-manager', ), 'be807912' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'phabricator-shaped-request', ), - 'c293f7b9' => + 'c4569c05' => array( - 0 => 'javelin-install', - 1 => 'javelin-event', + 0 => 'javelin-magical-init', + 1 => 'javelin-install', 2 => 'javelin-util', - 3 => 'javelin-magical-init', - ), - 'c54eeefb' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-vector', - 3 => 'javelin-util', + 3 => 'javelin-vector', + 4 => 'javelin-stratcom', ), 'c60f4327' => array( 0 => 'javelin-stratcom', 1 => 'javelin-install', 2 => 'javelin-uri', 3 => 'javelin-util', ), 'ca3f91eb' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'phabricator-phtize', ), - 'cd9e7094' => + 'd149e002' => array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-typeahead', - 3 => 'javelin-typeahead-ondemand-source', - 4 => 'javelin-dom', - ), - 'cdde23f1' => - array( - 0 => 'javelin-install', - 1 => 'javelin-typeahead-source', + 0 => 'javelin-stratcom', + 1 => 'javelin-request', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-install', + 5 => 'javelin-util', + 6 => 'javelin-mask', + 7 => 'javelin-uri', + 8 => 'javelin-routable', ), 'd19198c8' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-dynval', 4 => 'javelin-reactor-dom', ), 'd254d646' => array( 0 => 'javelin-util', ), 'd2907473' => array( 0 => 'javelin-dom', 1 => 'javelin-util', 2 => 'javelin-stratcom', 3 => 'javelin-install', 4 => 'javelin-workflow', 5 => 'javelin-router', 6 => 'javelin-behavior-device', 7 => 'javelin-vector', ), 'd4a14807' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-view', ), 'd4eecc63' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', ), 'd6f54db0' => array( 0 => 'javelin-behavior', 1 => 'javelin-request', 2 => 'javelin-stratcom', 3 => 'javelin-dom', ), 'd75709e6' => array( 0 => 'javelin-behavior', 1 => 'javelin-workflow', 2 => 'javelin-json', 3 => 'javelin-dom', 4 => 'phabricator-keyboard-shortcut', ), 'd835b03a' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'phabricator-shaped-request', ), - 'd9a9b862' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - ), 'dd7e8ef5' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-workflow', 3 => 'javelin-util', 4 => 'javelin-stratcom', ), + 'de2e896f' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-typeahead', + 3 => 'javelin-typeahead-ondemand-source', + 4 => 'javelin-dom', + ), 'e10f8e18' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'phabricator-prefab', ), 'e1ff79b1' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), 'e32d14ab' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'phabricator-phtize', 4 => 'phabricator-textareautils', 5 => 'javelin-workflow', 6 => 'javelin-vector', ), 'e379b58e' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-vector', 3 => 'javelin-dom', 4 => 'javelin-uri', ), + 'e566f52c' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + ), 'e5822781' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-json', 3 => 'javelin-workflow', 4 => 'javelin-magical-init', ), 'e5b406f9' => array( 0 => 'javelin-install', 1 => 'javelin-dom', 2 => 'javelin-view-visitor', 3 => 'javelin-util', ), - 'e7c21fb3' => - array( - 0 => 'javelin-dom', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - 3 => 'javelin-install', - ), 'e9581f08' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-workflow', 3 => 'javelin-dom', 4 => 'phabricator-draggable-list', ), 'efe49472' => array( 0 => 'javelin-install', 1 => 'javelin-util', ), 'f0a41b9f' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-stratcom', 4 => 'javelin-workflow', 5 => 'javelin-behavior-device', 6 => 'javelin-history', 7 => 'javelin-vector', 8 => 'phabricator-shaped-request', ), 'f2441746' => array( 0 => 'javelin-dom', 1 => 'javelin-util', 2 => 'javelin-stratcom', 3 => 'javelin-install', 4 => 'javelin-request', 5 => 'javelin-workflow', ), 'f36e01af' => array( 0 => 'javelin-behavior', 1 => 'javelin-behavior-device', 2 => 'javelin-stratcom', 3 => 'javelin-vector', 4 => 'phabricator-hovercard', ), 'f3fef818' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'phuix-dropdown-menu', 4 => 'phuix-action-list-view', 5 => 'phuix-action-view', 6 => 'javelin-workflow', ), 'f51afce0' => array( 0 => 'javelin-behavior', 1 => 'javelin-request', 2 => 'javelin-stratcom', 3 => 'javelin-vector', 4 => 'javelin-dom', 5 => 'javelin-uri', 6 => 'javelin-behavior-device', ), 'f588412e' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'phabricator-prefab', 4 => 'multirow-row-manager', 5 => 'javelin-json', ), 'f6555212' => array( 0 => 'javelin-install', 1 => 'javelin-reactornode', 2 => 'javelin-util', 3 => 'javelin-reactor', ), 'f726d506' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', 3 => 'javelin-history', ), 'f7379f45' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'phabricator-shaped-request', ), 'f7f1289f' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', ), 'f7fc67ec' => array( 0 => 'javelin-install', 1 => 'javelin-typeahead', 2 => 'javelin-dom', 3 => 'javelin-request', 4 => 'javelin-typeahead-ondemand-source', 5 => 'javelin-util', ), 'f8248bc5' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-json', 4 => 'javelin-stratcom', 5 => 'phabricator-shaped-request', ), 'f9539603' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-stratcom', 3 => 'javelin-uri', ), 'fa0f4fc2' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-vector', 3 => 'javelin-magical-init', ), 42126667 => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-request', ), 48086888 => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-workflow', ), 60479091 => array( 0 => 'phabricator-busy', 1 => 'javelin-behavior', ), 82439934 => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', 3 => 'javelin-stratcom', 4 => 'javelin-workflow', 5 => 'phabricator-draggable-list', ), ), 'packages' => array( 'core.pkg.css' => array( 0 => 'phabricator-core-css', 1 => 'phabricator-zindex-css', 2 => 'phui-button-css', 3 => 'phabricator-standard-page-view', 4 => 'aphront-dialog-view-css', 5 => 'phui-form-view-css', 6 => 'aphront-panel-view-css', 7 => 'aphront-table-view-css', 8 => 'aphront-tokenizer-control-css', 9 => 'aphront-typeahead-control-css', 10 => 'aphront-list-filter-view-css', 11 => 'phabricator-remarkup-css', 12 => 'syntax-highlighting-css', 13 => 'aphront-pager-view-css', 14 => 'phabricator-transaction-view-css', 15 => 'aphront-tooltip-css', 16 => 'phabricator-flag-css', 17 => 'aphront-error-view-css', 18 => 'sprite-gradient-css', 19 => 'sprite-menu-css', 20 => 'sprite-apps-css', 21 => 'sprite-apps-large-css', 22 => 'phabricator-main-menu-view', 23 => 'phabricator-notification-css', 24 => 'phabricator-notification-menu-css', 25 => 'lightbox-attachment-css', 26 => 'phui-header-view-css', 27 => 'phabricator-filetree-view-css', 28 => 'phabricator-nav-view-css', 29 => 'phabricator-side-menu-view-css', 30 => 'phabricator-crumbs-view-css', 31 => 'phui-object-item-list-view-css', 32 => 'global-drag-and-drop-css', 33 => 'phui-spacing-css', 34 => 'phui-form-css', 35 => 'phui-icon-view-css', 36 => 'phabricator-application-launch-view-css', 37 => 'phabricator-action-list-view-css', 38 => 'phui-property-list-view-css', 39 => 'phui-tag-view-css', 40 => 'phui-list-view-css', 41 => 'font-fontawesome', 42 => 'phui-font-icon-base-css', 43 => 'sprite-main-header-css', 44 => 'phui-box-css', 45 => 'phui-object-box-css', 46 => 'phui-timeline-view-css', 47 => 'sprite-tokens-css', 48 => 'tokens-css', 49 => 'phui-status-list-view-css', ), 'core.pkg.js' => array( 0 => 'javelin-util', 1 => 'javelin-install', 2 => 'javelin-event', 3 => 'javelin-stratcom', 4 => 'javelin-behavior', 5 => 'javelin-resource', 6 => 'javelin-request', 7 => 'javelin-vector', 8 => 'javelin-dom', 9 => 'javelin-json', 10 => 'javelin-uri', 11 => 'javelin-workflow', 12 => 'javelin-mask', 13 => 'javelin-typeahead', 14 => 'javelin-typeahead-normalizer', 15 => 'javelin-typeahead-source', 16 => 'javelin-typeahead-preloaded-source', 17 => 'javelin-typeahead-ondemand-source', 18 => 'javelin-tokenizer', 19 => 'javelin-history', 20 => 'javelin-router', 21 => 'javelin-routable', 22 => 'javelin-behavior-aphront-basic-tokenizer', 23 => 'javelin-behavior-workflow', 24 => 'javelin-behavior-aphront-form-disable-on-submit', 25 => 'phabricator-keyboard-shortcut-manager', 26 => 'phabricator-keyboard-shortcut', 27 => 'javelin-behavior-phabricator-keyboard-shortcuts', 28 => 'javelin-behavior-refresh-csrf', 29 => 'javelin-behavior-phabricator-watch-anchor', 30 => 'javelin-behavior-phabricator-autofocus', 31 => 'phuix-dropdown-menu', 32 => 'phuix-action-list-view', 33 => 'phuix-action-view', 34 => 'phabricator-phtize', 35 => 'javelin-behavior-phabricator-oncopy', 36 => 'phabricator-tooltip', 37 => 'javelin-behavior-phabricator-tooltips', 38 => 'phabricator-prefab', 39 => 'javelin-behavior-device', 40 => 'javelin-behavior-toggle-class', 41 => 'javelin-behavior-lightbox-attachments', 42 => 'phabricator-busy', 43 => 'javelin-aphlict', 44 => 'phabricator-notification', 45 => 'javelin-behavior-aphlict-listen', 46 => 'javelin-behavior-phabricator-search-typeahead', 47 => 'javelin-behavior-konami', 48 => 'javelin-behavior-aphlict-dropdown', 49 => 'javelin-behavior-history-install', 50 => 'javelin-behavior-phabricator-gesture', 51 => 'javelin-behavior-phabricator-active-nav', 52 => 'javelin-behavior-phabricator-nav', 53 => 'javelin-behavior-phabricator-remarkup-assist', 54 => 'phabricator-textareautils', 55 => 'phabricator-file-upload', 56 => 'javelin-behavior-global-drag-and-drop', 57 => 'javelin-behavior-phabricator-reveal-content', 58 => 'phabricator-hovercard', 59 => 'javelin-behavior-phabricator-hovercards', 60 => 'javelin-color', 61 => 'javelin-fx', 62 => 'phabricator-draggable-list', 63 => 'javelin-behavior-phabricator-transaction-list', 64 => 'javelin-behavior-phabricator-show-all-transactions', 65 => 'javelin-behavior-phui-timeline-dropdown-menu', 66 => 'javelin-behavior-doorkeeper-tag', ), 'darkconsole.pkg.js' => array( 0 => 'javelin-behavior-dark-console', 1 => 'javelin-behavior-error-log', ), 'differential.pkg.css' => array( 0 => 'differential-core-view-css', 1 => 'differential-changeset-view-css', 2 => 'differential-results-table-css', 3 => 'differential-revision-history-css', 4 => 'differential-revision-list-css', 5 => 'differential-table-of-contents-css', 6 => 'differential-revision-comment-css', 7 => 'differential-revision-add-comment-css', 8 => 'phabricator-object-selector-css', 9 => 'phabricator-content-source-view-css', 10 => 'inline-comment-summary-css', ), 'differential.pkg.js' => array( 0 => 'phabricator-drag-and-drop-file-upload', 1 => 'phabricator-shaped-request', 2 => 'javelin-behavior-differential-feedback-preview', 3 => 'javelin-behavior-differential-edit-inline-comments', 4 => 'javelin-behavior-differential-populate', 5 => 'javelin-behavior-differential-show-more', 6 => 'javelin-behavior-differential-diff-radios', 7 => 'javelin-behavior-differential-comment-jump', 8 => 'javelin-behavior-differential-add-reviewers-and-ccs', 9 => 'javelin-behavior-differential-keyboard-navigation', 10 => 'javelin-behavior-aphront-drag-and-drop-textarea', 11 => 'javelin-behavior-phabricator-object-selector', 12 => 'javelin-behavior-repository-crossreference', 13 => 'javelin-behavior-load-blame', 14 => 'differential-inline-comment-editor', 15 => 'javelin-behavior-differential-dropdown-menus', 16 => 'javelin-behavior-differential-toggle-files', 17 => 'javelin-behavior-differential-user-select', 18 => 'javelin-behavior-aphront-more', ), 'diffusion.pkg.css' => array( 0 => 'diffusion-commit-view-css', 1 => 'diffusion-icons-css', ), 'diffusion.pkg.js' => array( 0 => 'javelin-behavior-diffusion-pull-lastmodified', 1 => 'javelin-behavior-diffusion-commit-graph', 2 => 'javelin-behavior-audit-preview', ), 'maniphest.pkg.css' => array( 0 => 'maniphest-task-summary-css', ), 'maniphest.pkg.js' => array( 0 => 'javelin-behavior-maniphest-batch-selector', 1 => 'javelin-behavior-maniphest-transaction-controls', 2 => 'javelin-behavior-maniphest-transaction-preview', 3 => 'javelin-behavior-maniphest-transaction-expand', 4 => 'javelin-behavior-maniphest-subpriority-editor', 5 => 'javelin-behavior-maniphest-list-editor', ), ), ); diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index 99359970f0..91030d5749 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -1,242 +1,249 @@ buildStandardPageView(); $page->setApplicationName(pht('Login')); $page->setBaseURI('/login/'); $page->setTitle(idx($data, 'title')); $page->appendChild($view); $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } protected function renderErrorPage($title, array $messages) { $view = new AphrontErrorView(); $view->setTitle($title); $view->setErrors($messages); return $this->buildApplicationPage( $view, array( 'title' => $title, )); } /** * Returns true if this install is newly setup (i.e., there are no user * accounts yet). In this case, we enter a special mode to permit creation * of the first account form the web UI. */ protected function isFirstTimeSetup() { // If there are any auth providers, this isn't first time setup, even if // we don't have accounts. if (PhabricatorAuthProvider::getAllEnabledProviders()) { return false; } // Otherwise, check if there are any user accounts. If not, we're in first // time setup. $any_users = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setLimit(1) ->execute(); return !$any_users; } /** * Log a user into a web session and return an @{class:AphrontResponse} which * corresponds to continuing the login process. * * Normally, this is a redirect to the validation controller which makes sure * the user's cookies are set. However, event listeners can intercept this * event and do something else if they prefer. * * @param PhabricatorUser User to log the viewer in as. * @return AphrontResponse Response which continues the login process. */ protected function loginUser(PhabricatorUser $user) { $response = $this->buildLoginValidateResponse($user); $session_type = PhabricatorAuthSession::TYPE_WEB; $event_type = PhabricatorEventType::TYPE_AUTH_WILLLOGINUSER; $event_data = array( 'user' => $user, 'type' => $session_type, 'response' => $response, 'shouldLogin' => true, ); $event = id(new PhabricatorEvent($event_type, $event_data)) ->setUser($user); PhutilEventEngine::dispatchEvent($event); $should_login = $event->getValue('shouldLogin'); if ($should_login) { $session_key = id(new PhabricatorAuthSessionEngine()) ->establishSession($session_type, $user->getPHID(), $partial = true); // NOTE: We allow disabled users to login and roadblock them later, so // there's no check for users being disabled here. $request = $this->getRequest(); $request->setCookie( PhabricatorCookies::COOKIE_USERNAME, $user->getUsername()); $request->setCookie( PhabricatorCookies::COOKIE_SESSION, $session_key); $this->clearRegistrationCookies(); } return $event->getValue('response'); } protected function clearRegistrationCookies() { $request = $this->getRequest(); // Clear the registration key. $request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION); // Clear the client ID / OAuth state key. $request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID); } private function buildLoginValidateResponse(PhabricatorUser $user) { $validate_uri = new PhutilURI($this->getApplicationURI('validate/')); $validate_uri->setQueryParam('expect', $user->getUsername()); return id(new AphrontRedirectResponse())->setURI((string)$validate_uri); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Error'), array( $message, )); } protected function loadAccountForRegistrationOrLinking($account_key) { $request = $this->getRequest(); $viewer = $request->getUser(); $account = null; $provider = null; $response = null; if (!$account_key) { $response = $this->renderError( pht('Request did not include account key.')); return array($account, $provider, $response); } // NOTE: We're using the omnipotent user because the actual user may not // be logged in yet, and because we want to tailor an error message to // distinguish between "not usable" and "does not exist". We do explicit // checks later on to make sure this account is valid for the intended - // operation. + // operation. This requires edit permission for completeness and consistency + // but it won't actually be meaningfully checked because we're using the + // ominpotent user. $account = id(new PhabricatorExternalAccountQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAccountSecrets(array($account_key)) ->needImages(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$account) { $response = $this->renderError(pht('No valid linkable account.')); return array($account, $provider, $response); } if ($account->getUserPHID()) { if ($account->getUserPHID() != $viewer->getPHID()) { $response = $this->renderError( pht( 'The account you are attempting to register or link is already '. 'linked to another user.')); } else { $response = $this->renderError( pht( 'The account you are attempting to link is already linked '. 'to your account.')); } return array($account, $provider, $response); } $registration_key = $request->getCookie( PhabricatorCookies::COOKIE_REGISTRATION); // NOTE: This registration key check is not strictly necessary, because // we're only creating new accounts, not linking existing accounts. It // might be more hassle than it is worth, especially for email. // // The attack this prevents is getting to the registration screen, then // copy/pasting the URL and getting someone else to click it and complete // the process. They end up with an account bound to credentials you // control. This doesn't really let you do anything meaningful, though, // since you could have simply completed the process yourself. if (!$registration_key) { $response = $this->renderError( pht( 'Your browser did not submit a registration key with the request. '. 'You must use the same browser to begin and complete registration. '. 'Check that cookies are enabled and try again.')); return array($account, $provider, $response); } // We store the digest of the key rather than the key itself to prevent a // theoretical attacker with read-only access to the database from // hijacking registration sessions. $actual = $account->getProperty('registrationKey'); $expect = PhabricatorHash::digest($registration_key); if ($actual !== $expect) { $response = $this->renderError( pht( 'Your browser submitted a different registration key than the one '. 'associated with this account. You may need to clear your cookies.')); return array($account, $provider, $response); } $other_account = id(new PhabricatorExternalAccount())->loadAllWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s AND id != %d', $account->getAccountType(), $account->getAccountDomain(), $account->getAccountID(), $account->getID()); if ($other_account) { $response = $this->renderError( pht( 'The account you are attempting to register with already belongs '. 'to another user.')); return array($account, $provider, $response); } $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $account->getProviderKey()); if (!$provider) { $response = $this->renderError( pht( 'The account you are attempting to register with uses a nonexistent '. 'or disabled authentication provider (with key "%s"). An '. 'administrator may have recently disabled this provider.', $account->getProviderKey())); return array($account, $provider, $response); } return array($account, $provider, null); } } diff --git a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php index fa7bb85360..27382eb48b 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php @@ -1,151 +1,156 @@ setName('refresh') ->setExamples('**refresh**') ->setSynopsis( pht( 'Refresh OAuth access tokens. This is primarily useful for '. 'development and debugging.')) ->setArguments( array( array( 'name' => 'user', 'param' => 'user', 'help' => 'Refresh tokens for a given user.', ), array( 'name' => 'type', 'param' => 'provider', 'help' => 'Refresh tokens for a given provider type.', ), array( 'name' => 'domain', 'param' => 'domain', 'help' => 'Refresh tokens for a given domain.', ), )); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $viewer = $this->getViewer(); $query = id(new PhabricatorExternalAccountQuery()) - ->setViewer($viewer); + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); $username = $args->getArg('user'); if (strlen($username)) { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withUsernames(array($username)) ->executeOne(); if ($user) { $query->withUserPHIDs(array($user->getPHID())); } else { throw new PhutilArgumentUsageException( pht('No such user "%s"!', $username)); } } $type = $args->getArg('type'); if (strlen($type)) { $query->withAccountTypes(array($type)); } $domain = $args->getArg('domain'); if (strlen($domain)) { $query->withAccountDomains(array($domain)); } $accounts = $query->execute(); if (!$accounts) { throw new PhutilArgumentUsageException( pht('No accounts match the arguments!')); } else { $console->writeOut( "%s\n", pht( 'Found %s account(s) to refresh.', new PhutilNumber(count($accounts)))); } $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($accounts as $account) { $console->writeOut( "%s\n", pht( 'Refreshing account #%d (%s/%s).', $account->getID(), $account->getAccountType(), $account->getAccountDomain())); $key = $account->getProviderKey(); if (empty($providers[$key])) { $console->writeOut( "> %s\n", pht('Skipping, provider is not enabled or does not exist.')); continue; } $provider = $providers[$key]; if (!($provider instanceof PhabricatorAuthProviderOAuth2)) { $console->writeOut( "> %s\n", pht('Skipping, provider is not an OAuth2 provider.')); continue; } $adapter = $provider->getAdapter(); if (!$adapter->supportsTokenRefresh()) { $console->writeOut( "> %s\n", pht('Skipping, provider does not support token refresh.')); continue; } $refresh_token = $account->getProperty('oauth.token.refresh'); if (!$refresh_token) { $console->writeOut( "> %s\n", pht('Skipping, provider has no stored refresh token.')); continue; } $console->writeOut( "+ %s\n", pht( 'Refreshing token, current token expires in %s seconds.', new PhutilNumber( $account->getProperty('oauth.token.access.expires') - time()))); $token = $provider->getOAuthAccessToken($account, $force_refresh = true); if (!$token) { $console->writeOut( "* %s\n", pht('Unable to refresh token!')); continue; } $console->writeOut( "+ %s\n", pht( 'Refreshed token, new token expires in %s seconds.', new PhutilNumber( $account->getProperty('oauth.token.access.expires') - time()))); } $console->writeOut("%s\n", pht('Done.')); return 0; } } diff --git a/src/applications/auth/query/PhabricatorExternalAccountQuery.php b/src/applications/auth/query/PhabricatorExternalAccountQuery.php index 0198d5bd38..36f63f78a5 100644 --- a/src/applications/auth/query/PhabricatorExternalAccountQuery.php +++ b/src/applications/auth/query/PhabricatorExternalAccountQuery.php @@ -1,201 +1,211 @@ userPHIDs = $user_phids; return $this; } public function withAccountIDs(array $account_ids) { $this->accountIDs = $account_ids; return $this; } public function withAccountDomains(array $account_domains) { $this->accountDomains = $account_domains; return $this; } public function withAccountTypes(array $account_types) { $this->accountTypes = $account_types; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withIDs($ids) { $this->ids = $ids; return $this; } public function withAccountSecrets(array $secrets) { $this->accountSecrets = $secrets; return $this; } public function needImages($need) { $this->needImages = $need; return $this; } public function loadPage() { $table = new PhabricatorExternalAccount(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } public function willFilterPage(array $accounts) { if ($this->needImages) { $file_phids = mpull($accounts, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { // NOTE: We use the omnipotent viewer here because these files are // usually created during registration and can't be associated with // the correct policies, since the relevant user account does not exist // yet. In effect, if you can see an ExternalAccount, you can see its // profile image. $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } $default_file = null; foreach ($accounts as $account) { $image_phid = $account->getProfileImagePHID(); if ($image_phid && isset($files[$image_phid])) { $account->attachProfileImageFile($files[$image_phid]); } else { if ($default_file === null) { $default_file = PhabricatorFile::loadBuiltin( $this->getViewer(), 'profile.png'); } $account->attachProfileImageFile($default_file); } } } return $accounts; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->accountTypes) { $where[] = qsprintf( $conn_r, 'accountType IN (%Ls)', $this->accountTypes); } if ($this->accountDomains) { $where[] = qsprintf( $conn_r, 'accountDomain IN (%Ls)', $this->accountDomains); } if ($this->accountIDs) { $where[] = qsprintf( $conn_r, 'accountID IN (%Ls)', $this->accountIDs); } if ($this->userPHIDs) { $where[] = qsprintf( $conn_r, 'userPHID IN (%Ls)', $this->userPHIDs); } if ($this->accountSecrets) { $where[] = qsprintf( $conn_r, 'accountSecret IN (%Ls)', $this->accountSecrets); } return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorApplicationPeople'; } /** * Attempts to find an external account and if none exists creates a new * external account with a shiny new ID and PHID. * * NOTE: This function assumes the first item in various query parameters is * the correct value to use in creating a new external account. */ public function loadOneOrCreate() { $account = $this->executeOne(); if (!$account) { $account = new PhabricatorExternalAccount(); if ($this->accountIDs) { $account->setAccountID(reset($this->accountIDs)); } if ($this->accountTypes) { $account->setAccountType(reset($this->accountTypes)); } if ($this->accountDomains) { $account->setAccountDomain(reset($this->accountDomains)); } if ($this->accountSecrets) { $account->setAccountSecret(reset($this->accountSecrets)); } if ($this->userPHIDs) { $account->setUserPHID(reset($this->userPHIDs)); } $account->save(); } return $account; } } diff --git a/src/applications/differential/landing/DifferentialLandingToGitHub.php b/src/applications/differential/landing/DifferentialLandingToGitHub.php index 74e2a2f689..81f98c2483 100644 --- a/src/applications/differential/landing/DifferentialLandingToGitHub.php +++ b/src/applications/differential/landing/DifferentialLandingToGitHub.php @@ -1,179 +1,184 @@ getUser(); $this->init($viewer, $repository); $workspace = $this->getGitWorkspace($repository); try { id(new DifferentialLandingToHostedGit()) ->commitRevisionToWorkspace( $revision, $workspace, $viewer); } catch (Exception $e) { throw new PhutilProxyException( 'Failed to commit patch', $e); } try { $this->pushWorkspaceRepository($repository, $workspace); } catch (Exception $e) { // If it's a permission problem, we know more than git. $dialog = $this->verifyRemotePermissions($viewer, $revision, $repository); if ($dialog) { return $dialog; } // Else, throw what git said. throw new PhutilProxyException( 'Failed to push changes upstream', $e); } } /** * returns PhabricatorActionView or an array of PhabricatorActionView or null. */ public function createMenuItem( PhabricatorUser $viewer, DifferentialRevision $revision, PhabricatorRepository $repository) { $vcs = $repository->getVersionControlSystem(); if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { return; } if ($repository->isHosted()) { return; } try { // These throw when failing. $this->init($viewer, $repository); $this->findGitHubRepo($repository); } catch (Exception $e) { return; } return $this->createActionView($revision, pht('Land to GitHub')) ->setIcon('fa-cloud-upload'); } public function pushWorkspaceRepository( PhabricatorRepository $repository, ArcanistRepositoryAPI $workspace) { $token = $this->getAccessToken(); $github_repo = $this->findGitHubRepo($repository); $remote = urisprintf( 'https://%s:x-oauth-basic@%s/%s.git', $token, $this->provider->getProviderDomain(), $github_repo); $workspace->execxLocal( 'push %P HEAD:master', new PhutilOpaqueEnvelope($remote)); } private function init($viewer, $repository) { $repo_uri = $repository->getRemoteURIObject(); $repo_domain = $repo_uri->getDomain(); $this->account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array('github')) ->withAccountDomains(array($repo_domain)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$this->account) { throw new Exception( "No matching GitHub account found for {$repo_domain}."); } $this->provider = PhabricatorAuthProvider::getEnabledProviderByKey( $this->account->getProviderKey()); if (!$this->provider) { throw new Exception("GitHub provider for {$repo_domain} is not enabled."); } } private function findGitHubRepo(PhabricatorRepository $repository) { $repo_uri = $repository->getRemoteURIObject(); $repo_path = $repo_uri->getPath(); if (substr($repo_path, -4) == '.git') { $repo_path = substr($repo_path, 0, -4); } $repo_path = ltrim($repo_path, '/'); return $repo_path; } private function getAccessToken() { return $this->provider->getOAuthAccessToken($this->account); } private function verifyRemotePermissions($viewer, $revision, $repository) { $github_user = $this->account->getUsername(); $github_repo = $this->findGitHubRepo($repository); $uri = urisprintf( 'https://api.github.com/repos/%s/collaborators/%s', $github_repo, $github_user); $uri = new PhutilURI($uri); $uri->setQueryParam('access_token', $this->getAccessToken()); list($status, $body, $headers) = id(new HTTPSFuture($uri))->resolve(); // Likely status codes: // 204 No Content: Has permissions. Token might be too weak. // 404 Not Found: Not a collaborator. // 401 Unauthorized: Token is bad/revoked. $no_permission = ($status->getStatusCode() == 404); if ($no_permission) { throw new Exception( "You don't have permission to push to this repository. \n". "Push permissions for this repository are managed on GitHub."); } $scopes = BaseHTTPFuture::getHeader($headers, 'X-OAuth-Scopes'); if (strpos($scopes, 'public_repo') === false) { $provider_key = $this->provider->getProviderKey(); $refresh_token_uri = new PhutilURI("/auth/refresh/{$provider_key}/"); $refresh_token_uri->setQueryParam('scope', 'public_repo'); return id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Stronger token needed')) ->appendChild(pht( 'In order to complete this action, you need a '. 'stronger GitHub token.')) ->setSubmitURI($refresh_token_uri) ->addCancelButton('/D'.$revision->getId()) ->setDisableWorkflowOnSubmit(true) ->addSubmitButton(pht('Refresh Account Link')); } } } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php index 855498f471..19191eb03c 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php @@ -1,124 +1,129 @@ getApplicationType() != self::APPTYPE_ASANA) { return false; } if ($ref->getApplicationDomain() != self::APPDOMAIN_ASANA) { return false; } $types = array( self::OBJTYPE_TASK => true, ); return isset($types[$ref->getObjectType()]); } public function pullRefs(array $refs) { $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); $viewer = $this->getViewer(); $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); if (!$provider) { return; } $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); if (!$accounts) { return $this->didFailOnMissingLink(); } // TODO: If the user has several linked Asana accounts, we just pick the // first one arbitrarily. We might want to try using all of them or do // something with more finesse. There's no UI way to link multiple accounts // right now so this is currently moot. $account = head($accounts); $token = $provider->getOAuthAccessToken($account); if (!$token) { return; } $template = id(new PhutilAsanaFuture()) ->setAccessToken($token); $futures = array(); foreach ($id_map as $key => $id) { $futures[$key] = id(clone $template) ->setRawAsanaQuery("tasks/{$id}"); } $results = array(); $failed = array(); foreach (Futures($futures) as $key => $future) { try { $results[$key] = $future->resolve(); } catch (Exception $ex) { if (($ex instanceof HTTPFutureResponseStatus) && ($ex->getStatusCode() == 404)) { // This indicates that the object has been deleted (or never existed, // or isn't visible to the current user) but it's a successful sync of // an object which isn't visible. } else { // This is something else, so consider it a synchronization failure. phlog($ex); $failed[$key] = $ex; } } } foreach ($refs as $ref) { $ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID())); $did_fail = idx($failed, $ref->getObjectKey()); if ($did_fail) { $ref->setSyncFailed(true); continue; } $result = idx($results, $ref->getObjectKey()); if (!$result) { continue; } $ref->setIsVisible(true); $ref->setAttribute('asana.data', $result); $ref->setAttribute('fullname', pht('Asana: %s', $result['name'])); $ref->setAttribute('title', $result['name']); $ref->setAttribute('description', $result['notes']); $obj = $ref->getExternalObject(); if ($obj->getID()) { continue; } $this->fillObjectFromData($obj, $result); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $obj->save(); unset($unguarded); } } public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { $id = $result['id']; $uri = "https://app.asana.com/0/{$id}/{$id}"; $obj->setObjectURI($uri); } } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php index 3cc8393f7e..9698237f1d 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php @@ -1,143 +1,148 @@ getApplicationType() != self::APPTYPE_JIRA) { return false; } $types = array( self::OBJTYPE_ISSUE => true, ); return isset($types[$ref->getObjectType()]); } public function pullRefs(array $refs) { $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); $viewer = $this->getViewer(); $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); if (!$provider) { return; } $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); if (!$accounts) { return $this->didFailOnMissingLink(); } // TODO: When we support multiple JIRA instances, we need to disambiguate // issues (perhaps with additional configuration) or cast a wide net // (by querying all instances). For now, just query the one instance. $account = head($accounts); $futures = array(); foreach ($id_map as $key => $id) { $futures[$key] = $provider->newJIRAFuture( $account, 'rest/api/2/issue/'.phutil_escape_uri($id), 'GET'); } $results = array(); $failed = array(); foreach (Futures($futures) as $key => $future) { try { $results[$key] = $future->resolveJSON(); } catch (Exception $ex) { if (($ex instanceof HTTPFutureResponseStatus) && ($ex->getStatusCode() == 404)) { // This indicates that the object has been deleted (or never existed, // or isn't visible to the current user) but it's a successful sync of // an object which isn't visible. } else { // This is something else, so consider it a synchronization failure. phlog($ex); $failed[$key] = $ex; } } } foreach ($refs as $ref) { $ref->setAttribute('name', pht('JIRA %s', $ref->getObjectID())); $did_fail = idx($failed, $ref->getObjectKey()); if ($did_fail) { $ref->setSyncFailed(true); continue; } $result = idx($results, $ref->getObjectKey()); if (!$result) { continue; } $fields = idx($result, 'fields', array()); $ref->setIsVisible(true); $ref->setAttribute( 'fullname', pht('JIRA %s %s', $result['key'], idx($fields, 'summary'))); $ref->setAttribute('title', idx($fields, 'summary')); $ref->setAttribute('description', idx($result, 'description')); $obj = $ref->getExternalObject(); if ($obj->getID()) { continue; } $this->fillObjectFromData($obj, $result); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $obj->save(); unset($unguarded); } } public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { // Convert the "self" URI, which points at the REST endpoint, into a // browse URI. $self = idx($result, 'self'); $object_id = $obj->getObjectID(); $uri = self::getJIRAIssueBrowseURIFromJIRARestURI($self, $object_id); if ($uri !== null) { $obj->setObjectURI($uri); } } public static function getJIRAIssueBrowseURIFromJIRARestURI( $uri, $object_id) { $uri = new PhutilURI($uri); // The JIRA install might not be at the domain root, so we may need to // keep an initial part of the path, like "/jira/". Find the API specific // part of the URI, strip it off, then replace it with the web version. $path = $uri->getPath(); $pos = strrpos($path, 'rest/api/2/issue/'); if ($pos === false) { return null; } $path = substr($path, 0, $pos); $path = $path.'browse/'.$object_id; $uri->setPath($path); return (string)$uri; } } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index 33b32dc28f..ca452a5ff8 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -1,148 +1,153 @@ newOption('asana.workspace-id', 'string', null) ->setSummary(pht('Asana Workspace ID to publish into.')) ->setDescription( pht( 'To enable synchronization into Asana, enter an Asana Workspace '. 'ID here.'. "\n\n". "NOTE: This feature is new and experimental.")), $this->newOption('asana.project-ids', 'wild', null) ->setSummary(pht('Optional Asana projects to use as application tags.')) ->setDescription( pht( 'When Phabricator creates tasks in Asana, it can add the tasks '. 'to Asana projects based on which application the corresponding '. 'object in Phabricator comes from. For example, you can add code '. 'reviews in Asana to a "Differential" project.'. "\n\n". 'NOTE: This feature is new and experimental.')) ); } public function renderContextualDescription( PhabricatorConfigOption $option, AphrontRequest $request) { switch ($option->getKey()) { case 'asana.workspace-id': break; case 'asana.project-ids': return $this->renderContextualProjectDescription($option, $request); default: return parent::renderContextualDescription($option, $request); } $viewer = $request->getUser(); $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); if (!$provider) { return null; } $account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$account) { return null; } $token = $provider->getOAuthAccessToken($account); if (!$token) { return null; } try { $workspaces = id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setRawAsanaQuery('workspaces') ->resolve(); } catch (Exception $ex) { return null; } if (!$workspaces) { return null; } $out = array(); $out[] = pht('| Workspace ID | Workspace Name |'); $out[] = '| ------------ | -------------- |'; foreach ($workspaces as $workspace) { $out[] = sprintf('| `%s` | `%s` |', $workspace['id'], $workspace['name']); } $out = implode("\n", $out); $out = pht( "The Asana Workspaces your linked account has access to are:\n\n%s", $out); return PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($out), 'default', $viewer); } private function renderContextualProjectDescription( PhabricatorConfigOption $option, AphrontRequest $request) { $viewer = $request->getUser(); $publishers = id(new PhutilSymbolLoader()) ->setAncestorClass('DoorkeeperFeedStoryPublisher') ->loadObjects(); $out = array(); $out[] = pht( 'To specify projects to add tasks to, enter a JSON map with publisher '. 'class names as keys and a list of project IDs as values. For example, '. 'to put Differential tasks into Asana projects with IDs `123` and '. '`456`, enter:'. "\n\n". " lang=txt\n". " {\n". " \"DifferentialDoorkeeperRevisionFeedStoryPublisher\" : [123, 456]\n". " }\n"); $out[] = pht('Available publishers class names are:'); foreach ($publishers as $publisher) { $out[] = ' - `'.get_class($publisher).'`'; } $out[] = pht( 'You can find an Asana project ID by clicking the project in Asana and '. 'then examining the URL:'. "\n\n". " lang=txt\n". " https://app.asana.com/0/12345678901234567890/111111111111111111\n". " ^^^^^^^^^^^^^^^^^^^^\n". " This is the ID to use.\n"); $out = implode("\n", $out); return PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($out), 'default', $viewer); } } diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php index 50a2dfabf2..4cb18ea80d 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php +++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php @@ -1,691 +1,701 @@ getWorkspaceID(); } /** * Publish stories into Asana using the Asana API. */ protected function publishFeedStory() { $story = $this->getFeedStory(); $data = $story->getStoryData(); $viewer = $this->getViewer(); $provider = $this->getProvider(); $workspace_id = $this->getWorkspaceID(); $object = $this->getStoryObject(); $src_phid = $object->getPHID(); $publisher = $this->getPublisher(); // Figure out all the users related to the object. Users go into one of // four buckets: // // - Owner: the owner of the object. This user becomes the assigned owner // of the parent task. // - Active: users who are responsible for the object and need to act on // it. For example, reviewers of a "needs review" revision. // - Passive: users who are responsible for the object, but do not need // to act on it right now. For example, reviewers of a "needs revision" // revision. // - Follow: users who are following the object; generally CCs. $owner_phid = $publisher->getOwnerPHID($object); $active_phids = $publisher->getActiveUserPHIDs($object); $passive_phids = $publisher->getPassiveUserPHIDs($object); $follow_phids = $publisher->getCCUserPHIDs($object); $all_phids = array(); $all_phids = array_merge( array($owner_phid), $active_phids, $passive_phids, $follow_phids); $all_phids = array_unique(array_filter($all_phids)); $phid_aid_map = $this->lookupAsanaUserIDs($all_phids); if (!$phid_aid_map) { throw new PhabricatorWorkerPermanentFailureException( 'No related users have linked Asana accounts.'); } $owner_asana_id = idx($phid_aid_map, $owner_phid); $all_asana_ids = array_select_keys($phid_aid_map, $all_phids); $all_asana_ids = array_values($all_asana_ids); // Even if the actor isn't a reviewer, etc., try to use their account so // we can post in the correct voice. If we miss, we'll try all the other // related users. $try_users = array_merge( array($data->getAuthorPHID()), array_keys($phid_aid_map)); $try_users = array_filter($try_users); $access_info = $this->findAnyValidAsanaAccessToken($try_users); list($possessed_user, $possessed_asana_id, $oauth_token) = $access_info; if (!$oauth_token) { throw new PhabricatorWorkerPermanentFailureException( 'Unable to find any Asana user with valid credentials to '. 'pull an OAuth token out of.'); } $etype_main = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANATASK; $etype_sub = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANASUBTASK; $equery = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes( array( $etype_main, $etype_sub, )) ->needEdgeData(true); $edges = $equery->execute(); $main_edge = head($edges[$src_phid][$etype_main]); $main_data = $this->getAsanaTaskData($object) + array( 'assignee' => $owner_asana_id, ); $projects = $this->getAsanaProjectIDs(); $extra_data = array(); if ($main_edge) { $extra_data = $main_edge['data']; $refs = id(new DoorkeeperImportEngine()) ->setViewer($possessed_user) ->withPHIDs(array($main_edge['dst'])) ->execute(); $parent_ref = head($refs); if (!$parent_ref) { throw new PhabricatorWorkerPermanentFailureException( 'DoorkeeperExternalObject could not be loaded.'); } if ($parent_ref->getSyncFailed()) { throw new Exception( 'Synchronization of parent task from Asana failed!'); } else if (!$parent_ref->getIsVisible()) { $this->log("Skipping main task update, object is no longer visible.\n"); $extra_data['gone'] = true; } else { $edge_cursor = idx($main_edge['data'], 'cursor', 0); // TODO: This probably breaks, very rarely, on 32-bit systems. if ($edge_cursor <= $story->getChronologicalKey()) { $this->log("Updating main task.\n"); $task_id = $parent_ref->getObjectID(); $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$parent_ref->getObjectID(), 'PUT', $main_data); } else { $this->log( "Skipping main task update, cursor is ahead of the story.\n"); } } } else { // If there are no followers (CCs), and no active or passive users // (reviewers or auditors), and we haven't synchronized the object before, // don't synchronize the object. if (!$active_phids && !$passive_phids && !$follow_phids) { $this->log("Object has no followers or active/passive users.\n"); return; } $parent = $this->makeAsanaAPICall( $oauth_token, 'tasks', 'POST', array( 'workspace' => $workspace_id, 'projects' => $projects, // NOTE: We initially create parent tasks in the "Later" state but // don't update it afterward, even if the corresponding object // becomes actionable. The expectation is that users will prioritize // tasks in responses to notifications of state changes, and that // we should not overwrite their choices. 'assignee_status' => 'later', ) + $main_data); $parent_ref = $this->newRefFromResult( DoorkeeperBridgeAsana::OBJTYPE_TASK, $parent); $extra_data = array( 'workspace' => $workspace_id, ); } // Synchronize main task followers. $task_id = $parent_ref->getObjectID(); // Reviewers are added as followers of the parent task silently, because // they receive a notification when they are assigned as the owner of their // subtask, so the follow notification is redundant / non-actionable. $silent_followers = array_select_keys($phid_aid_map, $active_phids) + array_select_keys($phid_aid_map, $passive_phids); $silent_followers = array_values($silent_followers); // CCs are added as followers of the parent task with normal notifications, // since they won't get a secondary subtask notification. $noisy_followers = array_select_keys($phid_aid_map, $follow_phids); $noisy_followers = array_values($noisy_followers); // To synchronize follower data, just add all the followers. The task might // have additional followers, but we can't really tell how they got there: // were they CC'd and then unsubscribed, or did they manually follow the // task? Assume the latter since it's easier and less destructive and the // former is rare. To be fully consistent, we should enumerate followers // and remove unknown followers, but that's a fair amount of work for little // benefit, and creates a wider window for race conditions. // Add the silent followers first so that a user who is both a reviewer and // a CC gets silently added and then implicitly skipped by then noisy add. // They will get a subtask notification. // We only do this if the task still exists. if (empty($extra_data['gone'])) { $this->addFollowers($oauth_token, $task_id, $silent_followers, true); $this->addFollowers($oauth_token, $task_id, $noisy_followers); // We're also going to synchronize project data here. $this->addProjects($oauth_token, $task_id, $projects); } $dst_phid = $parent_ref->getExternalObject()->getPHID(); // Update the main edge. $edge_data = array( 'cursor' => $story->getChronologicalKey(), ) + $extra_data; $edge_options = array( 'data' => $edge_data, ); id(new PhabricatorEdgeEditor()) ->setActor($viewer) ->addEdge($src_phid, $etype_main, $dst_phid, $edge_options) ->save(); if (!$parent_ref->getIsVisible()) { throw new PhabricatorWorkerPermanentFailureException( 'DoorkeeperExternalObject has no visible object on the other side; '. 'this likely indicates the Asana task has been deleted.'); } // Now, handle the subtasks. $sub_editor = id(new PhabricatorEdgeEditor()) ->setActor($viewer); // First, find all the object references in Phabricator for tasks that we // know about and import their objects from Asana. $sub_edges = $edges[$src_phid][$etype_sub]; $sub_refs = array(); $subtask_data = $this->getAsanaSubtaskData($object); $have_phids = array(); if ($sub_edges) { $refs = id(new DoorkeeperImportEngine()) ->setViewer($possessed_user) ->withPHIDs(array_keys($sub_edges)) ->execute(); foreach ($refs as $ref) { if ($ref->getSyncFailed()) { throw new Exception( 'Synchronization of child task from Asana failed!'); } if (!$ref->getIsVisible()) { $ref->getExternalObject()->delete(); continue; } $have_phids[$ref->getExternalObject()->getPHID()] = $ref; } } // Remove any edges in Phabricator which don't have valid tasks in Asana. // These are likely tasks which have been deleted. We're going to respawn // them. foreach ($sub_edges as $sub_phid => $sub_edge) { if (isset($have_phids[$sub_phid])) { continue; } $this->log( "Removing subtask edge to %s, foreign object is not visible.\n", $sub_phid); $sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid); unset($sub_edges[$sub_phid]); } // For each active or passive user, we're looking for an existing, valid // task. If we find one we're going to update it; if we don't, we'll // create one. We ignore extra subtasks that we didn't create (we gain // nothing by deleting them and might be nuking something important) and // ignore subtasks which have been moved across workspaces or replanted // under new parents (this stuff is too edge-casey to bother checking for // and complicated to fix, as it needs extra API calls). However, we do // clean up subtasks we created whose owners are no longer associated // with the object. $subtask_states = array_fill_keys($active_phids, false) + array_fill_keys($passive_phids, true); // Continue with only those users who have Asana credentials. $subtask_states = array_select_keys( $subtask_states, array_keys($phid_aid_map)); $need_subtasks = $subtask_states; $user_to_ref_map = array(); $nuke_refs = array(); foreach ($sub_edges as $sub_phid => $sub_edge) { $user_phid = idx($sub_edge['data'], 'userPHID'); if (isset($need_subtasks[$user_phid])) { unset($need_subtasks[$user_phid]); $user_to_ref_map[$user_phid] = $have_phids[$sub_phid]; } else { // This user isn't associated with the object anymore, so get rid // of their task and edge. $nuke_refs[$sub_phid] = $have_phids[$sub_phid]; } } // These are tasks we know about but which are no longer relevant -- for // example, because a user has been removed as a reviewer. Remove them and // their edges. foreach ($nuke_refs as $sub_phid => $ref) { $sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid); $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$ref->getObjectID(), 'DELETE', array()); $ref->getExternalObject()->delete(); } // For each user that we don't have a subtask for, create a new subtask. foreach ($need_subtasks as $user_phid => $is_completed) { $subtask = $this->makeAsanaAPICall( $oauth_token, 'tasks', 'POST', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], 'completed' => $is_completed, 'parent' => $parent_ref->getObjectID(), )); $subtask_ref = $this->newRefFromResult( DoorkeeperBridgeAsana::OBJTYPE_TASK, $subtask); $user_to_ref_map[$user_phid] = $subtask_ref; // We don't need to synchronize this subtask's state because we just // set it when we created it. unset($subtask_states[$user_phid]); // Add an edge to track this subtask. $sub_editor->addEdge( $src_phid, $etype_sub, $subtask_ref->getExternalObject()->getPHID(), array( 'data' => array( 'userPHID' => $user_phid, ), )); } // Synchronize all the previously-existing subtasks. foreach ($subtask_states as $user_phid => $is_completed) { $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$user_to_ref_map[$user_phid]->getObjectID(), 'PUT', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], 'completed' => $is_completed, )); } foreach ($user_to_ref_map as $user_phid => $ref) { // For each subtask, if the acting user isn't the same user as the subtask // owner, remove the acting user as a follower. Currently, the acting user // will be added as a follower only when they create the task, but this // may change in the future (e.g., closing the task may also mark them // as a follower). Wipe every subtask to be sure. The intent here is to // leave only the owner as a follower so that the acting user doesn't // receive notifications about changes to subtask state. Note that // removing followers is silent in all cases in Asana and never produces // any kind of notification, so this isn't self-defeating. if ($user_phid != $possessed_user->getPHID()) { $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$ref->getObjectID().'/removeFollowers', 'POST', array( 'followers' => array($possessed_asana_id), )); } } // Update edges on our side. $sub_editor->save(); // Don't publish the "create" story, since pushing the object into Asana // naturally generates a notification which effectively serves the same // purpose as the "create" story. Similarly, "close" stories generate a // close notification. if (!$publisher->isStoryAboutObjectCreation($object) && !$publisher->isStoryAboutObjectClosure($object)) { // Post the feed story itself to the main Asana task. We do this last // because everything else is idempotent, so this is the only effect we // can't safely run more than once. $text = $publisher ->setRenderWithImpliedContext(true) ->getStoryText($object); $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$parent_ref->getObjectID().'/stories', 'POST', array( 'text' => $text, )); } } /* -( Internals )---------------------------------------------------------- */ private function getWorkspaceID() { return PhabricatorEnv::getEnvConfig('asana.workspace-id'); } private function getProvider() { if (!$this->provider) { $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); if (!$provider) { throw new PhabricatorWorkerPermanentFailureException( 'No Asana provider configured.'); } $this->provider = $provider; } return $this->provider; } private function getAsanaTaskData($object) { $publisher = $this->getPublisher(); $title = $publisher->getObjectTitle($object); $uri = $publisher->getObjectURI($object); $description = $publisher->getObjectDescription($object); $is_completed = $publisher->isObjectClosed($object); $notes = array( $description, $uri, $this->getSynchronizationWarning(), ); $notes = implode("\n\n", $notes); return array( 'name' => $title, 'notes' => $notes, 'completed' => $is_completed, ); } private function getAsanaSubtaskData($object) { $publisher = $this->getPublisher(); $title = $publisher->getResponsibilityTitle($object); $uri = $publisher->getObjectURI($object); $description = $publisher->getObjectDescription($object); $notes = array( $description, $uri, $this->getSynchronizationWarning(), ); $notes = implode("\n\n", $notes); return array( 'name' => $title, 'notes' => $notes, ); } private function getSynchronizationWarning() { return "\xE2\x9A\xA0 DO NOT EDIT THIS TASK \xE2\x9A\xA0\n". "\xE2\x98\xA0 Your changes will not be reflected in Phabricator.\n". "\xE2\x98\xA0 Your changes will be destroyed the next time state ". "is synchronized."; } private function lookupAsanaUserIDs($all_phids) { $phid_map = array(); $all_phids = array_unique(array_filter($all_phids)); if (!$all_phids) { return $phid_map; } $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUserPHIDs($all_phids) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); foreach ($accounts as $account) { $phid_map[$account->getUserPHID()] = $account->getAccountID(); } // Put this back in input order. $phid_map = array_select_keys($phid_map, $all_phids); return $phid_map; } private function findAnyValidAsanaAccessToken(array $user_phids) { if (!$user_phids) { return array(null, null, null); } $provider = $this->getProvider(); $viewer = $this->getViewer(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs($user_phids) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); // Reorder accounts in the original order. // TODO: This needs to be adjusted if/when we allow you to link multiple // accounts. $accounts = mpull($accounts, null, 'getUserPHID'); $accounts = array_select_keys($accounts, $user_phids); $workspace_id = $this->getWorkspaceID(); foreach ($accounts as $account) { // Get a token if possible. $token = $provider->getOAuthAccessToken($account); if (!$token) { continue; } // Verify we can actually make a call with the token, and that the user // has access to the workspace in question. try { id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setRawAsanaQuery("workspaces/{$workspace_id}") ->resolve(); } catch (Exception $ex) { // This token didn't make it through; try the next account. continue; } $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($account->getUserPHID())) ->executeOne(); if ($user) { return array($user, $account->getAccountID(), $token); } } return array(null, null, null); } private function makeAsanaAPICall($token, $action, $method, array $params) { foreach ($params as $key => $value) { if ($value === null) { unset($params[$key]); } else if (is_array($value)) { unset($params[$key]); foreach ($value as $skey => $svalue) { $params[$key.'['.$skey.']'] = $svalue; } } } return id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setMethod($method) ->setRawAsanaQuery($action, $params) ->resolve(); } private function newRefFromResult($type, $result) { $ref = id(new DoorkeeperObjectRef()) ->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA) ->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA) ->setObjectType($type) ->setObjectID($result['id']) ->setIsVisible(true); $xobj = $ref->newExternalObject(); $ref->attachExternalObject($xobj); $bridge = new DoorkeeperBridgeAsana(); $bridge->fillObjectFromData($xobj, $result); $xobj->save(); return $ref; } private function addFollowers( $oauth_token, $task_id, array $followers, $silent = false) { if (!$followers) { return; } $data = array( 'followers' => $followers, ); // NOTE: This uses a currently-undocumented API feature to suppress the // follow notifications. if ($silent) { $data['silent'] = true; } $this->makeAsanaAPICall( $oauth_token, "tasks/{$task_id}/addFollowers", 'POST', $data); } private function getAsanaProjectIDs() { $project_ids = array(); $publisher = $this->getPublisher(); $config = PhabricatorEnv::getEnvConfig('asana.project-ids'); if (is_array($config)) { $ids = idx($config, get_class($publisher)); if (is_array($ids)) { foreach ($ids as $id) { if (is_scalar($id)) { $project_ids[] = $id; } } } } return $project_ids; } private function addProjects( $oauth_token, $task_id, array $project_ids) { foreach ($project_ids as $project_id) { $data = array('project' => $project_id); $this->makeAsanaAPICall( $oauth_token, "tasks/{$task_id}/addProject", 'POST', $data); } } } diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php index d6686b7de7..c7ba08dd70 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php +++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php @@ -1,169 +1,174 @@ getFeedStory(); $viewer = $this->getViewer(); $provider = $this->getProvider(); $object = $this->getStoryObject(); $publisher = $this->getPublisher(); $jira_issue_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE); if (!$jira_issue_phids) { $this->log("Story is about an object with no linked JIRA issues.\n"); return; } $xobjs = id(new DoorkeeperExternalObjectQuery()) ->setViewer($viewer) ->withPHIDs($jira_issue_phids) ->execute(); if (!$xobjs) { $this->log("Story object has no corresponding external JIRA objects.\n"); return; } $try_users = $this->findUsersToPossess(); if (!$try_users) { $this->log("No users to act on linked JIRA objects.\n"); return; } $story_text = $this->renderStoryText(); $xobjs = mgroup($xobjs, 'getApplicationDomain'); foreach ($xobjs as $domain => $xobj_list) { $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs($try_users) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($domain)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); // Reorder accounts in the original order. // TODO: This needs to be adjusted if/when we allow you to link multiple // accounts. $accounts = mpull($accounts, null, 'getUserPHID'); $accounts = array_select_keys($accounts, $try_users); foreach ($xobj_list as $xobj) { foreach ($accounts as $account) { try { $provider->newJIRAFuture( $account, 'rest/api/2/issue/'.$xobj->getObjectID().'/comment', 'POST', array( 'body' => $story_text, ))->resolveJSON(); break; } catch (HTTPFutureResponseStatus $ex) { phlog($ex); $this->log( "Failed to update object %s using user %s.\n", $xobj->getObjectID(), $account->getUserPHID()); } } } } } /* -( Internals )---------------------------------------------------------- */ /** * Get the active JIRA provider. * * @return PhabricatorAuthProviderOAuth1JIRA Active JIRA auth provider. * @task internal */ private function getProvider() { if (!$this->provider) { $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); if (!$provider) { throw new PhabricatorWorkerPermanentFailureException( 'No JIRA provider configured.'); } $this->provider = $provider; } return $this->provider; } /** * Get a list of users to act as when publishing into JIRA. * * @return list Candidate user PHIDs to act as when publishing this * story. * @task internal */ private function findUsersToPossess() { $object = $this->getStoryObject(); $publisher = $this->getPublisher(); $data = $this->getFeedStory()->getStoryData(); // Figure out all the users related to the object. Users go into one of // four buckets. For JIRA integration, we don't care about which bucket // a user is in, since we just want to publish an update to linked objects. $owner_phid = $publisher->getOwnerPHID($object); $active_phids = $publisher->getActiveUserPHIDs($object); $passive_phids = $publisher->getPassiveUserPHIDs($object); $follow_phids = $publisher->getCCUserPHIDs($object); $all_phids = array_merge( array($owner_phid), $active_phids, $passive_phids, $follow_phids); $all_phids = array_unique(array_filter($all_phids)); // Even if the actor isn't a reviewer, etc., try to use their account so // we can post in the correct voice. If we miss, we'll try all the other // related users. $try_users = array_merge( array($data->getAuthorPHID()), $all_phids); $try_users = array_filter($try_users); return $try_users; } private function renderStoryText() { $object = $this->getStoryObject(); $publisher = $this->getPublisher(); $text = $publisher->getStoryText($object); $uri = $publisher->getObjectURI($object); return $text."\n\n".$uri; } } diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php index fd0f0e1bb1..1fd6068468 100644 --- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php @@ -1,207 +1,212 @@ processReceivedMail($mail, $sender); } public function validateSender( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $failure_reason = null; if ($sender->getIsDisabled()) { $failure_reason = pht( 'Your account (%s) is disabled, so you can not interact with '. 'Phabricator over email.', $sender->getUsername()); } else if ($sender->getIsStandardUser()) { if (!$sender->getIsApproved()) { $failure_reason = pht( 'Your account (%s) has not been approved yet. You can not interact '. 'with Phabricator over email until your account is approved.', $sender->getUsername()); } else if (PhabricatorUserEmail::isEmailVerificationRequired() && !$sender->getIsEmailVerified()) { $failure_reason = pht( 'You have not verified the email address for your account (%s). '. 'You must verify your email address before you can interact '. 'with Phabricator over email.', $sender->getUsername()); } } if ($failure_reason) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER, $failure_reason); } } /** * Identifies the sender's user account for a piece of received mail. Note * that this method does not validate that the sender is who they say they * are, just that they've presented some credential which corresponds to a * recognizable user. */ public function loadSender(PhabricatorMetaMTAReceivedMail $mail) { $raw_from = $mail->getHeader('From'); $from = self::getRawAddress($raw_from); $reasons = array(); // Try to find a user with this email address. $user = PhabricatorUser::loadOneWithEmailAddress($from); if ($user) { return $user; } else { $reasons[] = pht( 'This email was sent from "%s", but that address is not recognized by '. 'Phabricator and does not correspond to any known user account.', $raw_from); } // If we missed on "From", try "Reply-To" if we're configured for it. $raw_reply_to = $mail->getHeader('Reply-To'); if (strlen($raw_reply_to)) { $reply_to_key = 'metamta.insecure-auth-with-reply-to'; $allow_reply_to = PhabricatorEnv::getEnvConfig($reply_to_key); if ($allow_reply_to) { $reply_to = self::getRawAddress($raw_reply_to); $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); if ($user) { return $user; } else { $reasons[] = pht( 'Phabricator is configured to authenticate users using the '. '"Reply-To" header, but the reply address ("%s") on this '. 'message does not correspond to any known user account.', $raw_reply_to); } } else { $reasons[] = pht( '(Phabricator is not configured to authenticate users using the '. '"Reply-To" header, so it was ignored.)'); } } // If we don't know who this user is, load or create an external user // account for them if we're configured for it. $email_key = 'phabricator.allow-email-users'; $allow_email_users = PhabricatorEnv::getEnvConfig($email_key); if ($allow_email_users) { $from_obj = new PhutilEmailAddress($from); $xuser = id(new PhabricatorExternalAccountQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAccountTypes(array('email')) ->withAccountDomains(array($from_obj->getDomainName(), 'self')) ->withAccountIDs(array($from_obj->getAddress())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->loadOneOrCreate(); return $xuser->getPhabricatorUser(); } else { $reasons[] = pht( 'Phabricator is also not configured to allow unknown external users '. 'to send mail to the system using just an email address.'); $reasons[] = pht( 'To interact with Phabricator, add this address ("%s") to your '. 'account.', $raw_from); } $reasons = implode("\n\n", $reasons); throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER, $reasons); } /** * Determine if two inbound email addresses are effectively identical. This * method strips and normalizes addresses so that equivalent variations are * correctly detected as identical. For example, these addresses are all * considered to match one another: * * "Abraham Lincoln" * alincoln@example.com * * "Abraham" # With configured prefix. * * @param string Email address. * @param string Another email address. * @return bool True if addresses match. */ public static function matchAddresses($u, $v) { $u = self::getRawAddress($u); $v = self::getRawAddress($v); $u = self::stripMailboxPrefix($u); $v = self::stripMailboxPrefix($v); return ($u === $v); } /** * Strip a global mailbox prefix from an address if it is present. Phabricator * can be configured to prepend a prefix to all reply addresses, which can * make forwarding rules easier to write. A prefix looks like: * * example@phabricator.example.com # No Prefix * phabricator+example@phabricator.example.com # Prefix "phabricator" * * @param string Email address, possibly with a mailbox prefix. * @return string Email address with any prefix stripped. */ public static function stripMailboxPrefix($address) { $address = id(new PhutilEmailAddress($address))->getAddress(); $prefix_key = 'metamta.single-reply-handler-prefix'; $prefix = PhabricatorEnv::getEnvConfig($prefix_key); $len = strlen($prefix); if ($len) { $prefix = $prefix.'+'; $len = $len + 1; } if ($len) { if (!strncasecmp($address, $prefix, $len)) { $address = substr($address, strlen($prefix)); } } return $address; } /** * Reduce an email address to its canonical form. For example, an adddress * like: * * "Abraham Lincoln" < ALincoln@example.com > * * ...will be reduced to: * * alincoln@example.com * * @param string Email address in noncanonical form. * @return string Canonical email address. */ public static function getRawAddress($address) { $address = id(new PhutilEmailAddress($address))->getAddress(); return trim(phutil_utf8_strtolower($address)); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php index 82426c51b4..afee9ea84f 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php @@ -1,293 +1,298 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$user) { return new Aphront404Response(); } $profile_uri = '/p/'.$user->getUsername().'/'; $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new profile picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } } if (!$errors) { if ($is_default) { $user->setProfileImagePHID(null); } else { $user->setProfileImagePHID($xformed->getPHID()); $xformed->attachToObject($viewer, $user->getPHID()); } $user->save(); return id(new AphrontRedirectResponse())->setURI($profile_uri); } } $title = pht('Edit Profile Picture'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($user->getUsername(), $profile_uri); $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); $default_image = PhabricatorFile::loadBuiltin($viewer, 'profile.png'); $images = array(); $current = $user->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } // Try to add external account images for any associated external accounts. $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($user->getPHID())) ->needImages(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); foreach ($accounts as $account) { $file = $account->getProfileImageFile(); if ($account->getProfileImagePHID() != $file->getPHID()) { // This is a default image, just skip it. continue; } $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $account->getProviderKey()); if ($provider) { $tip = pht('Picture From %s', $provider->getProviderName()); } else { $tip = pht('Picture From External Account'); } if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => $tip, ); } } // Try to add Gravatar images for any email addresses associated with the // account. if (PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) { $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $user->getPHID()); $futures = array(); foreach ($emails as $email_object) { $email = $email_object->getAddress(); $hash = md5(strtolower(trim($email))); $uri = id(new PhutilURI("https://secure.gravatar.com/avatar/{$hash}")) ->setQueryParams( array( 'size' => 200, 'default' => '404', 'rating' => 'x', )); $futures[$email] = new HTTPSFuture($uri); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (Futures($futures) as $email => $future) { try { list($body) = $future->resolvex(); $file = PhabricatorFile::newFromFileData( $body, array( 'name' => 'profile-gravatar', 'ttl' => (60 * 60 * 4), )); if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Gravatar for %s', $email), ); } } catch (Exception $ex) { // Just continue. } } unset($unguarded); } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), $button); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue($buttons)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($profile_uri) ->setValue(pht('Upload Picture'))); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/people/storage/PhabricatorExternalAccount.php b/src/applications/people/storage/PhabricatorExternalAccount.php index 94ca59fb02..92a22d0c7d 100644 --- a/src/applications/people/storage/PhabricatorExternalAccount.php +++ b/src/applications/people/storage/PhabricatorExternalAccount.php @@ -1,110 +1,140 @@ assertAttached($this->profileImageFile); } public function attachProfileImageFile(PhabricatorFile $file) { $this->profileImageFile = $file; return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeoplePHIDTypeExternal::TYPECONST); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function getPhabricatorUser() { $tmp_usr = id(new PhabricatorUser()) ->makeEphemeral() ->setPHID($this->getPHID()); return $tmp_usr; } public function getProviderKey() { return $this->getAccountType().':'.$this->getAccountDomain(); } public function save() { if (!$this->getAccountSecret()) { $this->setAccountSecret(Filesystem::readRandomCharacters(32)); } return parent::save(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function isUsableForLogin() { $key = $this->getProviderKey(); $provider = PhabricatorAuthProvider::getEnabledProviderByKey($key); if (!$provider) { return false; } if (!$provider->shouldAllowLogin()) { return false; } return true; } + public function getDisplayName() { + if (strlen($this->displayName)) { + return $this->displayName; + } + + // TODO: Figure out how much identifying information we're going to show + // to users about external accounts. For now, just show a string which is + // clearly not an error, but don't disclose any identifying information. + + $map = array( + 'email' => pht('Email User'), + ); + + $type = $this->getAccountType(); + + return idx($map, $type, pht('"%s" User', $type)); + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - return PhabricatorPolicies::POLICY_NOONE; + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return PhabricatorPolicies::POLICY_NOONE; + } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getUserPHID()); } public function describeAutomaticCapability($capability) { - // TODO: (T603) This is complicated. - return null; + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return null; + case PhabricatorPolicyCapability::CAN_EDIT: + return pht( + 'External accounts can only be edited by the account owner.'); + } } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php b/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php index 5faaf5672d..929ce9c3f3 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php @@ -1,145 +1,150 @@ getUser(); $providers = PhabricatorAuthProvider::getAllProviders(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->needImages(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); $linked_head = id(new PHUIHeaderView()) ->setHeader(pht('Linked Accounts and Authentication')); $linked = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setFlush(true) ->setNoDataString(pht('You have no linked accounts.')); $login_accounts = 0; foreach ($accounts as $account) { if ($account->isUsableForLogin()) { $login_accounts++; } } foreach ($accounts as $account) { $item = id(new PHUIObjectItemView()); $provider = idx($providers, $account->getProviderKey()); if ($provider) { $item->setHeader($provider->getProviderName()); $can_unlink = $provider->shouldAllowAccountUnlink(); if (!$can_unlink) { $item->addAttribute(pht('Permanently Linked')); } } else { $item->setHeader( pht('Unknown Account ("%s")', $account->getProviderKey())); $can_unlink = true; } $can_login = $account->isUsableForLogin(); if (!$can_login) { $item->addAttribute( pht( 'Disabled (an administrator has disabled login for this '. 'account provider).')); } $can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1)); $can_refresh = $provider && $provider->shouldAllowAccountRefresh(); if ($can_refresh) { $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-refresh') ->setHref('/auth/refresh/'.$account->getProviderKey().'/')); } $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->setWorkflow(true) ->setDisabled(!$can_unlink) ->setHref('/auth/unlink/'.$account->getProviderKey().'/')); if ($provider) { $provider->willRenderLinkedAccount($viewer, $item, $account); } $linked->addItem($item); } $linkable_head = id(new PHUIHeaderView()) ->setHeader(pht('Add External Account')); $linkable = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setFlush(true) ->setNoDataString( pht('Your account is linked with all available providers.')); $accounts = mpull($accounts, null, 'getProviderKey'); $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $providers = msort($providers, 'getProviderName'); foreach ($providers as $key => $provider) { if (isset($accounts[$key])) { continue; } if (!$provider->shouldAllowAccountLink()) { continue; } $link_uri = '/auth/link/'.$provider->getProviderKey().'/'; $item = id(new PHUIObjectItemView()); $item->setHeader($provider->getProviderName()); $item->setHref($link_uri); $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-link') ->setHref($link_uri)); $linkable->addItem($item); } $linked_box = id(new PHUIObjectBoxView()) ->setHeader($linked_head) ->appendChild($linked); $linkable_box = id(new PHUIObjectBoxView()) ->setHeader($linkable_head) ->appendChild($linkable); return array( $linked_box, $linkable_box, ); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php b/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php index abbd38be25..4103f6c9ab 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php @@ -1,142 +1,147 @@ getUser(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->execute(); $identity_phids = mpull($accounts, 'getPHID'); $identity_phids[] = $viewer->getPHID(); $sessions = id(new PhabricatorAuthSessionQuery()) ->setViewer($viewer) ->withIdentityPHIDs($identity_phids) ->execute(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($identity_phids) ->execute(); $current_key = PhabricatorHash::digest( $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); $rows = array(); $rowc = array(); foreach ($sessions as $session) { if ($session->getSessionKey() == $current_key) { $rowc[] = 'highlighted'; $button = phutil_tag( 'a', array( 'class' => 'small grey button disabled', ), pht('Current')); } else { $rowc[] = null; $button = javelin_tag( 'a', array( 'href' => '/auth/session/terminate/'.$session->getID().'/', 'class' => 'small grey button', 'sigil' => 'workflow', ), pht('Terminate')); } $hisec = ($session->getHighSecurityUntil() - time()); $rows[] = array( $handles[$session->getUserPHID()]->renderLink(), substr($session->getSessionKey(), 0, 6), $session->getType(), ($hisec > 0) ? phabricator_format_relative_time($hisec) : null, phabricator_datetime($session->getSessionStart(), $viewer), phabricator_date($session->getSessionExpires(), $viewer), $button, ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht("You don't have any active sessions.")); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Identity'), pht('Session'), pht('Type'), pht('HiSec'), pht('Created'), pht('Expires'), pht(''), )); $table->setColumnClasses( array( 'wide', 'n', '', 'right', 'right', 'right', 'action', )); $terminate_icon = id(new PHUIIconView()) ->setIconFont('fa-exclamation-triangle'); $terminate_button = id(new PHUIButtonView()) ->setText(pht('Terminate All Sessions')) ->setHref('/auth/session/terminate/all/') ->setTag('a') ->setWorkflow(true) ->setIcon($terminate_icon); $header = id(new PHUIHeaderView()) ->setHeader(pht('Active Login Sessions')) ->addActionLink($terminate_button); $hisec = ($viewer->getSession()->getHighSecurityUntil() - time()); if ($hisec > 0) { $hisec_icon = id(new PHUIIconView()) ->setIconFont('fa-lock'); $hisec_button = id(new PHUIButtonView()) ->setText(pht('Leave High Security')) ->setHref('/auth/session/downgrade/') ->setTag('a') ->setWorkflow(true) ->setIcon($hisec_icon); $header->addActionLink($hisec_button); } $panel = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table); return $panel; } }