Page MenuHomestyx hydra

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 148df0336b..0bc98c6f64 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2160 +1,2160 @@
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* @generated
*/
return array(
'names' => array(
'core.pkg.css' => 'b5618e4c',
'core.pkg.js' => 'e64447dc',
'darkconsole.pkg.js' => 'df001cab',
'differential.pkg.css' => '8af45893',
'differential.pkg.js' => '42c10e78',
'diffusion.pkg.css' => '591664fa',
'diffusion.pkg.js' => 'bfc0737b',
'maniphest.pkg.css' => 'e34dfbec',
'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' => '9c279160',
'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/table-view.css' => 'b22b7216',
'rsrc/css/aphront/tokenizer.css' => '82ce2142',
'rsrc/css/aphront/tooltip.css' => '4099b97e',
'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' => '03fdd0a2',
'rsrc/css/application/base/notification-menu.css' => '6aa0a74b',
'rsrc/css/application/base/phabricator-application-launch-view.css' => '5d71008f',
'rsrc/css/application/base/standard-page-view.css' => '3f5b9311',
'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' => '8f852bc0',
'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' => 'b2b71e76',
'rsrc/css/application/differential/core.css' => '7ac3cabc',
'rsrc/css/application/differential/results-table.css' => '181aa9d9',
'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' => '63f3ef4a',
'rsrc/css/application/diffusion/diffusion-icons.css' => '9c5828da',
'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661',
'rsrc/css/application/feed/feed.css' => '7bfc6f12',
'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad',
'rsrc/css/application/flag/flag.css' => '5337623f',
'rsrc/css/application/harbormaster/harbormaster.css' => '49d64eb4',
'rsrc/css/application/herald/herald-test.css' => '778b008e',
'rsrc/css/application/herald/herald.css' => '826075fa',
'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' => '13ed8360',
'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' => '0d5f6498',
'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' => '95174bdd',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => 'b25b4beb',
'rsrc/css/application/phortune/phortune.css' => '9149f103',
'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
'rsrc/css/application/phriction/phriction-document-css.css' => '7d7f0071',
'rsrc/css/application/policy/policy-edit.css' => '815c66f7',
'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43',
'rsrc/css/application/policy/policy.css' => '957ea14c',
'rsrc/css/application/ponder/comments.css' => '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' => '28f433ef',
'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' => '29524ba4',
'rsrc/css/core/syntax.css' => '56c1ba38',
'rsrc/css/core/z-index.css' => '44e1d311',
'rsrc/css/diviner/diviner-shared.css' => '38813222',
'rsrc/css/font/font-awesome.css' => '0c10d96b',
'rsrc/css/font/font-source-sans-pro.css' => '91d53463',
'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3',
'rsrc/css/layout/phabricator-crumbs-view.css' => 'a49339de',
'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' => '8675968e',
'rsrc/css/phui/phui-action-header-view.css' => '89c497e7',
'rsrc/css/phui/phui-action-list.css' => '9ee9910a',
'rsrc/css/phui/phui-box.css' => '7b3a2eed',
'rsrc/css/phui/phui-button.css' => '008ba5e2',
'rsrc/css/phui/phui-document.css' => 'a5615198',
'rsrc/css/phui/phui-feed-story.css' => 'dd3c5ff5',
'rsrc/css/phui/phui-fontkit.css' => '9c3d2dce',
'rsrc/css/phui/phui-form-view.css' => 'aad06f2a',
'rsrc/css/phui/phui-form.css' => 'b78ec020',
'rsrc/css/phui/phui-header-view.css' => '39594ac0',
'rsrc/css/phui/phui-icon.css' => 'b4963a4f',
'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' => 'a2e6046f',
'rsrc/css/phui/phui-pinboard-view.css' => '3dd4a269',
'rsrc/css/phui/phui-property-list-view.css' => '86f9df88',
'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b',
'rsrc/css/phui/phui-spacing.css' => '042804d6',
'rsrc/css/phui/phui-status.css' => '888cedb8',
'rsrc/css/phui/phui-tag-view.css' => '6b74282b',
'rsrc/css/phui/phui-text.css' => 'cf019f54',
'rsrc/css/phui/phui-timeline-view.css' => '26bb3fd4',
'rsrc/css/phui/phui-workboard-view.css' => '2bf82d00',
'rsrc/css/phui/phui-workpanel-view.css' => '198c7e6c',
'rsrc/css/sprite-apps-large.css' => '20ec0cc0',
'rsrc/css/sprite-apps.css' => 'd5baed0f',
'rsrc/css/sprite-conpherence.css' => '3b4a0487',
'rsrc/css/sprite-docs.css' => '5f65d0da',
'rsrc/css/sprite-gradient.css' => '4bdb98a7',
'rsrc/css/sprite-login.css' => 'a355d921',
'rsrc/css/sprite-main-header.css' => '92720ee2',
'rsrc/css/sprite-menu.css' => 'afaf676d',
'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' => '579d3140',
'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => 'fe26fcb5',
'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '64677b24',
'rsrc/externals/font/sourcesans/SourceSansPro.woff' => '3614608c',
'rsrc/externals/font/sourcesans/SourceSansProBold.woff' => 'cbf46566',
'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' => '1ffb3a9c',
'rsrc/externals/javelin/core/util.js' => 'e7995242',
'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' => 'c4569c05',
'rsrc/externals/javelin/lib/History.js' => 'c60f4327',
'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' => '6eff08aa',
'rsrc/externals/javelin/lib/Vector.js' => 'cc1bd0b0',
'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' => '61cbc29a',
'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '9f06389f',
'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'e614d22b',
'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '1c22377d',
'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' => 'fcba4ecc',
'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/favicons/apple-touch-icon-120x120.png' => '43742962',
'rsrc/favicons/apple-touch-icon-152x152.png' => '669eaec3',
'rsrc/favicons/apple-touch-icon-76x76.png' => 'ecdef672',
'rsrc/favicons/favicon-128.png' => '47cdff03',
'rsrc/favicons/favicon-16x16.png' => 'ee2523ac',
'rsrc/favicons/favicon-32x32.png' => 'b6a8150e',
'rsrc/favicons/favicon-96x96.png' => '8f7ea177',
'rsrc/image/BFCFDA.png' => 'd5ec91f4',
'rsrc/image/actions/edit.png' => '2fc41442',
'rsrc/image/avatar.png' => '3eb28cd9',
'rsrc/image/checker_dark.png' => 'd8e65881',
'rsrc/image/checker_light.png' => 'a0155918',
'rsrc/image/checker_lighter.png' => 'd5da91b6',
'rsrc/image/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' => '58294cf3',
'rsrc/image/sprite-apps-X4.png' => '936fc9af',
'rsrc/image/sprite-apps-large-X2.png' => '79e15268',
'rsrc/image/sprite-apps-large.png' => '4d41b94a',
'rsrc/image/sprite-apps-xlarge.png' => 'a751a580',
'rsrc/image/sprite-apps.png' => '5570df20',
'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' => '5ae6de3a',
'rsrc/image/sprite-login.png' => '07f2c67c',
'rsrc/image/sprite-main-header.png' => '83521873',
'rsrc/image/sprite-menu-X2.png' => 'a54606dd',
'rsrc/image/sprite-menu.png' => '34048681',
'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' => '7d4cc76c',
'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' => '453c5375',
'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' => '6932def3',
'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' => '2c426492',
'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/herald/HeraldRuleEditor.js' => '335fd41f',
'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' => 'fe9a552f',
'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' => '0676345e',
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
'rsrc/js/application/releeph/releeph-request-state-change.js' => 'ab836011',
'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-show-older-transactions.js' => 'c30ccda9',
+ 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '83458cbb',
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9f7309fb',
'rsrc/js/application/transactions/behavior-transaction-list.js' => '13c739ea',
'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' => '8c49f386',
'rsrc/js/core/DraggableList.js' => 'a16ec1c6',
'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' => 'b5d57730',
'rsrc/js/core/Notification.js' => '0c6946e7',
'rsrc/js/core/Prefab.js' => 'bbae734c',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '5c93c52c',
'rsrc/js/core/Title.js' => '5c1c758c',
'rsrc/js/core/ToolTip.js' => '031d4411',
'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' => 'c51ae228',
'rsrc/js/core/behavior-file-tree.js' => '88236f00',
'rsrc/js/core/behavior-form.js' => '5c54cbf3',
'rsrc/js/core/behavior-gesture.js' => '3ab51e2c',
'rsrc/js/core/behavior-global-drag-and-drop.js' => '07f199d8',
'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' => '49b73b36',
'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' => '724b1247',
'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6',
'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' => '2bfa2836',
'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-table-view-css' => 'b22b7216',
'aphront-tokenizer-control-css' => '82ce2142',
'aphront-tooltip-css' => '4099b97e',
'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' => 'b2b71e76',
'differential-core-view-css' => '7ac3cabc',
'differential-inline-comment-editor' => 'f2441746',
'differential-results-table-css' => '181aa9d9',
'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' => '63f3ef4a',
'diffusion-icons-css' => '9c5828da',
'diffusion-source-css' => '66fdf661',
'diviner-shared-css' => '38813222',
'font-fontawesome' => '0c10d96b',
'font-source-sans-pro' => '91d53463',
'global-drag-and-drop-css' => '697324ad',
'harbormaster-css' => '49d64eb4',
'herald-css' => '826075fa',
'herald-rule-editor' => '335fd41f',
'herald-test-css' => '778b008e',
'inline-comment-summary-css' => '8cfd34e8',
'javelin-aphlict' => '4a07e8e3',
'javelin-behavior' => '61cbc29a',
'javelin-behavior-aphlict-dropdown' => '7d4cc76c',
'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' => '5c54cbf3',
'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' => '453c5375',
'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' => '6932def3',
'javelin-behavior-differential-keyboard-navigation' => '2c426492',
'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' => 'c51ae228',
'javelin-behavior-global-drag-and-drop' => '07f199d8',
'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' => '49b73b36',
'javelin-behavior-phabricator-oncopy' => '2926fff2',
'javelin-behavior-phabricator-remarkup-assist' => 'e32d14ab',
'javelin-behavior-phabricator-reveal-content' => '60821bc7',
'javelin-behavior-phabricator-search-typeahead' => '724b1247',
'javelin-behavior-phabricator-show-all-transactions' => '7c273581',
- 'javelin-behavior-phabricator-show-older-transactions' => 'c30ccda9',
+ 'javelin-behavior-phabricator-show-older-transactions' => '83458cbb',
'javelin-behavior-phabricator-tooltips' => '3ee3408b',
'javelin-behavior-phabricator-transaction-comment-form' => '9f7309fb',
'javelin-behavior-phabricator-transaction-list' => '13c739ea',
'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' => '2bfa2836',
'javelin-behavior-phui-timeline-dropdown-menu' => '4d94d9c3',
'javelin-behavior-policy-control' => 'f3fef818',
'javelin-behavior-policy-rule-editor' => 'fe9a552f',
'javelin-behavior-ponder-votebox' => '4e9b766b',
'javelin-behavior-project-boards' => '0676345e',
'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' => 'de2e896f',
'javelin-behavior-remarkup-preview' => 'f7379f45',
'javelin-behavior-reorder-applications' => '76b9fc3e',
'javelin-behavior-reorder-columns' => 'e1d25dfb',
'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' => 'e566f52c',
'javelin-behavior-view-placeholder' => '2fa810fc',
'javelin-behavior-workflow' => '0a3f3021',
'javelin-color' => '7e41274a',
'javelin-cookie' => '6b3dcf44',
'javelin-diffusion-locate-file-source' => 'b42eddc7',
'javelin-dom' => 'c4569c05',
'javelin-dynval' => 'f6555212',
'javelin-event' => '85ea0626',
'javelin-fx' => '54b612ba',
'javelin-history' => 'c60f4327',
'javelin-install' => '1ffb3a9c',
'javelin-json' => '69adf288',
'javelin-magical-init' => 'b88ab49e',
'javelin-mask' => '8a41885b',
'javelin-reactor' => '77b1cf6f',
'javelin-reactor-dom' => 'b6d401d6',
'javelin-reactor-node-calmer' => '76f4ebed',
'javelin-reactornode' => 'b4c30592',
'javelin-request' => '97258e55',
'javelin-resource' => '0f81f8df',
'javelin-routable' => 'b3e7d692',
'javelin-router' => '29274e2b',
'javelin-stratcom' => '8b0ad945',
'javelin-tokenizer' => '9f06389f',
'javelin-typeahead' => 'e614d22b',
'javelin-typeahead-composite-source' => '503e17fd',
'javelin-typeahead-normalizer' => '1c22377d',
'javelin-typeahead-ondemand-source' => '8b3fd187',
'javelin-typeahead-preloaded-source' => '54f314a0',
'javelin-typeahead-source' => 'fcba4ecc',
'javelin-typeahead-static-source' => '316b8fa1',
'javelin-uri' => '6eff08aa',
'javelin-util' => 'e7995242',
'javelin-vector' => 'cc1bd0b0',
'javelin-view' => '0f764c35',
'javelin-view-html' => 'e5b406f9',
'javelin-view-interpreter' => '0c33c1a0',
'javelin-view-renderer' => '6c2b09a2',
'javelin-view-visitor' => 'efe49472',
'javelin-workflow' => 'd149e002',
'lightbox-attachment-css' => '7acac05d',
'maniphest-batch-editor' => '8f380ebc',
'maniphest-report-css' => '6fc16517',
'maniphest-task-edit-css' => '8e23031b',
'maniphest-task-summary-css' => '13ed8360',
'multirow-row-manager' => 'b5d57730',
'owners-path-editor' => 'aa1733d0',
'owners-path-editor-css' => '2f00933b',
'paste-css' => 'aa1767d1',
'path-typeahead' => 'f7fc67ec',
'people-profile-css' => '0d5f6498',
'phabricator-action-list-view-css' => '9ee9910a',
'phabricator-application-launch-view-css' => '5d71008f',
'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' => 'a49339de',
'phabricator-dashboard-css' => 'a2bfdcbf',
'phabricator-drag-and-drop-file-upload' => '8c49f386',
'phabricator-draggable-list' => 'a16ec1c6',
'phabricator-fatal-config-template-css' => '25d446d6',
'phabricator-feed-css' => '7bfc6f12',
'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' => '03fdd0a2',
'phabricator-nav-view-css' => '9283c2df',
'phabricator-notification' => '0c6946e7',
'phabricator-notification-css' => '9c279160',
'phabricator-notification-menu-css' => '6aa0a74b',
'phabricator-object-selector-css' => '029a133d',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => 'bbae734c',
'phabricator-profile-css' => '28f433ef',
'phabricator-remarkup-css' => '29524ba4',
'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' => '3f5b9311',
'phabricator-textareautils' => '5c93c52c',
'phabricator-title' => '5c1c758c',
'phabricator-tooltip' => '031d4411',
'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' => '44e1d311',
'phame-css' => '19ecc703',
'pholio-css' => '95174bdd',
'pholio-edit-css' => '3ad9d1ee',
'pholio-inline-comments-css' => '8e545e49',
'phortune-credit-card-form' => '2290aeef',
'phortune-credit-card-form-css' => 'b25b4beb',
'phortune-css' => '9149f103',
'phrequent-css' => 'ffc185ad',
'phriction-document-css' => '7d7f0071',
'phui-action-header-view-css' => '89c497e7',
'phui-box-css' => '7b3a2eed',
'phui-button-css' => '008ba5e2',
'phui-calendar-css' => '8675968e',
'phui-calendar-day-css' => 'de035c8a',
'phui-calendar-list-css' => 'c1d0ca59',
'phui-calendar-month-css' => 'a92e47d2',
'phui-document-view-css' => 'a5615198',
'phui-feed-story-css' => 'dd3c5ff5',
'phui-font-icon-base-css' => '3dad2ae3',
'phui-fontkit-css' => '9c3d2dce',
'phui-form-css' => 'b78ec020',
'phui-form-view-css' => 'aad06f2a',
'phui-header-view-css' => '39594ac0',
'phui-icon-view-css' => 'b4963a4f',
'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' => 'a2e6046f',
'phui-pinboard-view-css' => '3dd4a269',
'phui-property-list-view-css' => '86f9df88',
'phui-remarkup-preview-css' => '19ad512b',
'phui-spacing-css' => '042804d6',
'phui-status-list-view-css' => '888cedb8',
'phui-tag-view-css' => '6b74282b',
'phui-text-css' => 'cf019f54',
'phui-timeline-view-css' => '26bb3fd4',
'phui-workboard-view-css' => '2bf82d00',
'phui-workpanel-view-css' => '198c7e6c',
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => '6e8cefa4',
'phuix-dropdown-menu' => 'bd4c8dca',
'policy-css' => '957ea14c',
'policy-edit-css' => '815c66f7',
'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' => '8f852bc0',
'sprite-apps-css' => 'd5baed0f',
'sprite-apps-large-css' => '20ec0cc0',
'sprite-conpherence-css' => '3b4a0487',
'sprite-docs-css' => '5f65d0da',
'sprite-gradient-css' => '4bdb98a7',
'sprite-login-css' => 'a355d921',
'sprite-main-header-css' => '92720ee2',
'sprite-menu-css' => 'afaf676d',
'sprite-payments-css' => 'cc085d44',
'sprite-projects-css' => '7578fa56',
'sprite-tokens-css' => '1706b943',
'syntax-highlighting-css' => '56c1ba38',
'tokens-css' => '3d0f239e',
),
'requires' => array(
'00861799' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-util',
'javelin-vector',
'differential-inline-comment-editor',
),
'029a133d' => array(
'aphront-dialog-view-css',
),
'031d4411' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
),
'03d6ed07' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
'javelin-install',
),
'065227cc' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
),
'0676345e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
'06e05112' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
),
'0720f2cf' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phabricator-busy',
),
'07f199d8' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-mask',
'phabricator-drag-and-drop-file-upload',
),
'0a3f3021' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'javelin-router',
),
'0c33c1a0' => array(
'javelin-view',
'javelin-install',
'javelin-dom',
),
'0c6946e7' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'0ec56e1d' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'phuix-dropdown-menu',
),
'0f764c35' => array(
'javelin-install',
'javelin-util',
),
'0f81f8df' => array(
'javelin-util',
'javelin-uri',
'javelin-install',
),
'13c739ea' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'javelin-uri',
'phabricator-textareautils',
),
'14a827de' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-json',
'phabricator-draggable-list',
),
'14d7a8b8' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-dom',
'javelin-magical-init',
'javelin-vector',
'javelin-request',
'javelin-util',
),
'152178f0' => array(
'javelin-behavior',
'javelin-util',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
'javelin-magical-init',
'javelin-request',
'javelin-history',
'javelin-workflow',
'javelin-mask',
'javelin-behavior-device',
'phabricator-keyboard-shortcut',
),
'1693a296' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'1ae869f2' => array(
'javelin-install',
'javelin-util',
'phabricator-keyboard-shortcut-manager',
),
'1c22377d' => array(
'javelin-install',
),
'1def2711' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'1ffb3a9c' => array(
'javelin-util',
'javelin-magical-init',
),
'2290aeef' => array(
'javelin-install',
'javelin-dom',
'javelin-json',
'javelin-workflow',
'javelin-util',
),
'22e16ae7' => array(
'javelin-behavior',
'javelin-dom',
'javelin-vector',
),
'2926fff2' => array(
'javelin-behavior',
'javelin-dom',
),
'29274e2b' => array(
'javelin-install',
'javelin-util',
),
'2b228192' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-json',
),
'2bfa2836' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'2c426492' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'phabricator-keyboard-shortcut',
),
'2fa810fc' => array(
'javelin-behavior',
'javelin-dom',
'javelin-view-renderer',
'javelin-install',
),
'316b8fa1' => array(
'javelin-install',
'javelin-typeahead-source',
),
'335fd41f' => array(
'multirow-row-manager',
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-json',
'phabricator-prefab',
),
'357b6e9b' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-util',
'javelin-dom',
'javelin-request',
'phabricator-keyboard-shortcut',
),
'361e3ed3' => array(
'javelin-behavior',
'javelin-dom',
),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-magical-init',
),
'3b3e1664' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'3d51a746' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'javelin-uri',
),
'3ee3408b' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'phabricator-tooltip',
),
'40a6a403' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'40b1ff90' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-notification',
'javelin-behavior-device',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
),
'44168bad' => array(
'javelin-behavior',
'javelin-dom',
'phabricator-prefab',
),
'453c5375' => array(
'javelin-behavior',
'javelin-dom',
),
'469c0d9e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
),
'47c794d8' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'49b73b36' => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
'javelin-util',
),
'4a07e8e3' => array(
'javelin-install',
'javelin-util',
),
'4d94d9c3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
'4e3e79a6' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'4e9b766b' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-request',
),
'4fdb476d' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'503e17fd' => array(
'javelin-install',
'javelin-typeahead-source',
'javelin-util',
),
'519705ea' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'54b612ba' => array(
'javelin-color',
'javelin-install',
'javelin-util',
),
'54f314a0' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-typeahead-source',
),
'558829c2' => array(
'javelin-stratcom',
'javelin-behavior',
'javelin-vector',
'javelin-dom',
),
'58f7803f' => array(
'javelin-behavior',
'javelin-aphlict',
'phabricator-phtize',
'javelin-dom',
),
'59b251eb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
),
'5bc2cb21' => array(
'javelin-behavior',
'javelin-stratcom',
),
'5c1c758c' => array(
'javelin-install',
),
'5c54cbf3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'5c93c52c' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
),
'5fefb143' => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
'javelin-stratcom',
),
'60821bc7' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'6153c708' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-workflow',
),
'61cbc29a' => array(
'javelin-magical-init',
'javelin-util',
),
'6453c869' => array(
'javelin-install',
'javelin-dom',
'javelin-fx',
),
'6932def3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-request',
'javelin-util',
'phabricator-shaped-request',
),
'69adf288' => array(
'javelin-install',
),
'6b3dcf44' => array(
'javelin-install',
'javelin-util',
),
'6c2b09a2' => array(
'javelin-install',
'javelin-util',
),
'6d3e1947' => array(
'javelin-behavior',
'javelin-diffusion-locate-file-source',
'javelin-dom',
'javelin-typeahead',
'javelin-uri',
),
'6e8cefa4' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
),
'6eff08aa' => array(
'javelin-install',
'javelin-util',
'javelin-stratcom',
),
'710f209e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phabricator-phtize',
'changeset-view-manager',
),
'724b1247' => array(
'javelin-behavior',
'javelin-typeahead-ondemand-source',
'javelin-typeahead',
'javelin-dom',
'javelin-uri',
'javelin-util',
'javelin-stratcom',
'phabricator-prefab',
),
'7319e029' => array(
'javelin-behavior',
'javelin-dom',
),
'76b9fc3e' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'76f4ebed' => array(
'javelin-install',
'javelin-reactor',
'javelin-util',
),
'77b1cf6f' => array(
'javelin-install',
'javelin-util',
),
'7814b593' => array(
'javelin-request',
'javelin-behavior',
'javelin-dom',
'javelin-router',
'javelin-util',
'phabricator-busy',
),
'7a68dda3' => array(
'owners-path-editor',
'javelin-behavior',
),
'7a9677fc' => array(
'phabricator-notification',
'javelin-stratcom',
'javelin-behavior',
),
'7b98d7c5' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
),
'7c273581' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'7cbe244b' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-router',
),
'7d4cc76c' => array(
'javelin-behavior',
'javelin-request',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
'javelin-behavior-device',
'phabricator-title',
),
'7e41274a' => array(
'javelin-install',
),
'7e8468ae' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'7ebaeed3' => array(
'herald-rule-editor',
'javelin-behavior',
),
'7ee2b591' => array(
'javelin-behavior',
'javelin-history',
),
'82ce2142' => array(
'aphront-typeahead-control-css',
),
+ '83458cbb' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ 'phabricator-busy',
+ ),
'84845b5b' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
'85ab3c8e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
),
'85ea0626' => array(
'javelin-install',
),
'88236f00' => array(
'javelin-behavior',
'phabricator-keyboard-shortcut',
'javelin-stratcom',
),
'886fd850' => array(
'javelin-install',
'javelin-reactor-dom',
'javelin-view-html',
'javelin-view-interpreter',
'javelin-view-renderer',
),
'8a41885b' => array(
'javelin-install',
'javelin-dom',
),
'8b0ad945' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'8b3fd187' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-typeahead-source',
),
'8c49f386' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-dom',
'javelin-uri',
'phabricator-file-upload',
),
'8ef9ab58' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'8fc1c918' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
'92eb531d' => array(
'javelin-behavior',
'javelin-dom',
'phabricator-drag-and-drop-file-upload',
'phabricator-textareautils',
),
'9414ff18' => array(
'javelin-behavior',
'javelin-resource',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
),
'97258e55' => array(
'javelin-install',
'javelin-stratcom',
'javelin-util',
'javelin-behavior',
'javelin-json',
'javelin-dom',
'javelin-resource',
'javelin-routable',
),
'988040b4' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'9c2623f4' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-workflow',
'phabricator-phtize',
'phabricator-drag-and-drop-file-upload',
'phabricator-draggable-list',
),
'9db3d160' => array(
'javelin-behavior',
'javelin-vector',
'javelin-dom',
),
'9f06389f' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
),
'9f7309fb' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-request',
'phabricator-shaped-request',
),
'a155550f' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'a16ec1c6' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'javelin-vector',
'javelin-magical-init',
),
'a4ae61bf' => array(
'javelin-install',
'javelin-dom',
'phabricator-notification',
),
'a5d7cf86' => array(
'javelin-dom',
),
'a80d0378' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'a826c925' => array(
'javelin-behavior',
'javelin-aphlict',
'javelin-stratcom',
'javelin-request',
'javelin-uri',
'javelin-dom',
'javelin-json',
'javelin-router',
'javelin-util',
'phabricator-notification',
),
'a8d8459d' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'a8da01f0' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-keyboard-shortcut',
),
'a9f88de2' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-fx',
'javelin-util',
),
'aa1733d0' => array(
'multirow-row-manager',
'javelin-install',
'path-typeahead',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
),
'ab836011' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-keyboard-shortcut',
),
'ab8d2723' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'ad7a69ca' => array(
'javelin-install',
'javelin-util',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
),
'b1f0ccee' => array(
'javelin-install',
'javelin-dom',
'javelin-reactor-dom',
),
'b2b4fbaf' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-request',
),
'b3a4b884' => array(
'javelin-behavior',
'phabricator-prefab',
),
'b3e7d692' => array(
'javelin-install',
),
'b42eddc7' => array(
'javelin-install',
'javelin-dom',
'javelin-typeahead-preloaded-source',
'javelin-util',
),
'b4c30592' => array(
'javelin-install',
'javelin-reactor',
'javelin-util',
'javelin-reactor-node-calmer',
),
'b5c256b8' => array(
'javelin-install',
'javelin-dom',
),
'b5d57730' => array(
'javelin-install',
'javelin-stratcom',
'javelin-dom',
'javelin-util',
),
'b6d401d6' => array(
'javelin-dom',
'javelin-dynval',
'javelin-reactor',
'javelin-reactornode',
'javelin-install',
'javelin-util',
),
'bba9eedf' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'bbae734c' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-typeahead',
'javelin-tokenizer',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
),
'bd4c8dca' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
'javelin-stratcom',
),
'bdaf4d04' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-request',
),
'bdb3e4d0' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'phabricator-tooltip',
'changeset-view-manager',
),
'be807912' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
- 'c30ccda9' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
- 'phabricator-busy',
- ),
'c4569c05' => array(
'javelin-magical-init',
'javelin-install',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
),
'c51ae228' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'c60f4327' => array(
'javelin-stratcom',
'javelin-install',
'javelin-uri',
'javelin-util',
),
'ca3f91eb' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'phabricator-phtize',
),
'cc1bd0b0' => array(
'javelin-install',
'javelin-event',
),
'd149e002' => array(
'javelin-stratcom',
'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
'javelin-util',
'javelin-mask',
'javelin-uri',
'javelin-routable',
),
'd19198c8' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-dynval',
'javelin-reactor-dom',
),
'd254d646' => array(
'javelin-util',
),
'd2907473' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
),
'd4a14807' => array(
'javelin-install',
'javelin-dom',
'javelin-view',
),
'd4eecc63' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'd6f54db0' => array(
'javelin-behavior',
'javelin-request',
'javelin-stratcom',
'javelin-dom',
),
'd75709e6' => array(
'javelin-behavior',
'javelin-workflow',
'javelin-json',
'javelin-dom',
'phabricator-keyboard-shortcut',
),
'd835b03a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
'dd7e8ef5' => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
'javelin-util',
'javelin-stratcom',
),
'de2e896f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-typeahead',
'javelin-typeahead-ondemand-source',
'javelin-dom',
),
'e10f8e18' => array(
'javelin-behavior',
'javelin-dom',
'phabricator-prefab',
),
'e1d25dfb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'e1ff79b1' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'e32d14ab' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-phtize',
'phabricator-textareautils',
'javelin-workflow',
'javelin-vector',
),
'e379b58e' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
),
'e566f52c' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'e5822781' => array(
'javelin-behavior',
'javelin-dom',
'javelin-json',
'javelin-workflow',
'javelin-magical-init',
),
'e5b406f9' => array(
'javelin-install',
'javelin-dom',
'javelin-view-visitor',
'javelin-util',
),
'e614d22b' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-util',
),
'e9581f08' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'efe49472' => array(
'javelin-install',
'javelin-util',
),
'f0a41b9f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'javelin-behavior-device',
'javelin-history',
'javelin-vector',
'phabricator-shaped-request',
),
'f2441746' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-request',
'javelin-workflow',
),
'f36e01af' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phabricator-hovercard',
),
'f3fef818' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'javelin-workflow',
),
'f588412e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
'multirow-row-manager',
'javelin-json',
),
'f6555212' => array(
'javelin-install',
'javelin-reactornode',
'javelin-util',
'javelin-reactor',
),
'f726d506' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'f7379f45' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
'f7f1289f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'f7fc67ec' => array(
'javelin-install',
'javelin-typeahead',
'javelin-dom',
'javelin-request',
'javelin-typeahead-ondemand-source',
'javelin-util',
),
'f8248bc5' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-json',
'javelin-stratcom',
'phabricator-shaped-request',
),
'f9539603' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-uri',
),
'fa0f4fc2' => array(
'javelin-behavior',
'javelin-dom',
'javelin-vector',
'javelin-magical-init',
),
'fcba4ecc' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-typeahead-normalizer',
),
'fe9a552f' => array(
'javelin-behavior',
'multirow-row-manager',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
'javelin-json',
),
42126667 => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
),
48086888 => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
),
60479091 => array(
'phabricator-busy',
'javelin-behavior',
),
82439934 => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
),
'packages' => array(
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'aphront-pager-view-css',
'phabricator-transaction-view-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'aphront-error-view-css',
'sprite-gradient-css',
'sprite-menu-css',
'sprite-apps-css',
'sprite-apps-large-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'lightbox-attachment-css',
'phui-header-view-css',
'phabricator-filetree-view-css',
'phabricator-nav-view-css',
'phabricator-side-menu-view-css',
'phabricator-crumbs-view-css',
'phui-object-item-list-view-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-application-launch-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'phui-font-icon-base-css',
'sprite-main-header-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'sprite-tokens-css',
'tokens-css',
'phui-status-list-view-css',
),
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-konami',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phabricator-hovercard',
'javelin-behavior-phabricator-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-all-transactions',
'javelin-behavior-phui-timeline-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
),
'darkconsole.pkg.js' => array(
'javelin-behavior-dark-console',
'javelin-behavior-error-log',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-results-table-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-edit-inline-comments',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-show-more',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-differential-comment-jump',
'javelin-behavior-differential-add-reviewers-and-ccs',
'javelin-behavior-differential-keyboard-navigation',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-load-blame',
'differential-inline-comment-editor',
'javelin-behavior-differential-dropdown-menus',
'javelin-behavior-differential-toggle-files',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-transaction-controls',
'javelin-behavior-maniphest-transaction-preview',
'javelin-behavior-maniphest-transaction-expand',
'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
),
);
diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php
index 75d78a24aa..0cca45e961 100644
--- a/src/applications/almanac/storage/AlmanacBinding.php
+++ b/src/applications/almanac/storage/AlmanacBinding.php
@@ -1,187 +1,194 @@
<?php
final class AlmanacBinding
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
AlmanacPropertyInterface {
protected $servicePHID;
protected $devicePHID;
protected $interfacePHID;
protected $mailKey;
private $service = self::ATTACHABLE;
private $device = self::ATTACHABLE;
private $interface = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewBinding(AlmanacService $service) {
return id(new AlmanacBinding())
->setServicePHID($service->getPHID())
->attachAlmanacProperties(array());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_service' => array(
'columns' => array('servicePHID', 'interfacePHID'),
'unique' => true,
),
'key_device' => array(
'columns' => array('devicePHID'),
),
'key_interface' => array(
'columns' => array('interfacePHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacBindingPHIDType::TYPECONST);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/binding/'.$this->getID().'/';
}
public function getService() {
return $this->assertAttached($this->service);
}
public function attachService(AlmanacService $service) {
$this->service = $service;
return $this;
}
public function getDevice() {
return $this->assertAttached($this->device);
}
public function attachDevice(AlmanacDevice $device) {
$this->device = $device;
return $this;
}
public function getInterface() {
return $this->assertAttached($this->interface);
}
public function attachInterface(AlmanacInterface $interface) {
$this->interface = $interface;
return $this;
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getService()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getService()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return array(
pht('A binding inherits the policies of its service.'),
pht(
'To view a binding, you must also be able to view its device and '.
'interface.'),
);
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'AlmanacCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacBindingEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacBindingTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
}
diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php
index 9075c1fd7e..692206f0dd 100644
--- a/src/applications/almanac/storage/AlmanacDevice.php
+++ b/src/applications/almanac/storage/AlmanacDevice.php
@@ -1,178 +1,185 @@
<?php
final class AlmanacDevice
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorSSHPublicKeyInterface,
AlmanacPropertyInterface {
protected $name;
protected $nameIndex;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
private $customFields = self::ATTACHABLE;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewDevice() {
return id(new AlmanacDevice())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->attachAlmanacProperties(array());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'nameIndex' => 'bytes12',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('nameIndex'),
'unique' => true,
),
'key_nametext' => array(
'columns' => array('name'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacDevicePHIDType::TYPECONST);
}
public function save() {
AlmanacNames::validateServiceOrDeviceName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName());
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/device/view/'.$this->getName().'/';
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'AlmanacCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacDeviceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacDeviceTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
return $this->getURI();
}
public function getSSHKeyDefaultName() {
return $this->getName();
}
}
diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php
index e91bab4cf4..663f815e02 100644
--- a/src/applications/almanac/storage/AlmanacNetwork.php
+++ b/src/applications/almanac/storage/AlmanacNetwork.php
@@ -1,90 +1,97 @@
<?php
final class AlmanacNetwork
extends AlmanacDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
public static function initializeNewNetwork() {
return id(new AlmanacNetwork())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'mailKey' => 'bytes20',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacNetworkPHIDType::TYPECONST);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/network/view/'.$this->getName().'/';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacNetworkEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacNetworkTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php
index 0d15afb15e..b058c116be 100644
--- a/src/applications/almanac/storage/AlmanacService.php
+++ b/src/applications/almanac/storage/AlmanacService.php
@@ -1,164 +1,171 @@
<?php
final class AlmanacService
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
AlmanacPropertyInterface {
protected $name;
protected $nameIndex;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
private $customFields = self::ATTACHABLE;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewService() {
return id(new AlmanacService())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->attachAlmanacProperties(array());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'nameIndex' => 'bytes12',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('nameIndex'),
'unique' => true,
),
'key_nametext' => array(
'columns' => array('name'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacServicePHIDType::TYPECONST);
}
public function save() {
AlmanacNames::validateServiceOrDeviceName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName());
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/service/view/'.$this->getName().'/';
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'AlmanacCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacServiceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacServiceTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
}
diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
index 3ae36034c3..957e195fbc 100644
--- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
+++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
@@ -1,128 +1,135 @@
<?php
final class PhabricatorAuthProviderConfig
extends PhabricatorAuthDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $providerClass;
protected $providerType;
protected $providerDomain;
protected $isEnabled;
protected $shouldAllowLogin = 0;
protected $shouldAllowRegistration = 0;
protected $shouldAllowLink = 0;
protected $shouldAllowUnlink = 0;
protected $shouldTrustEmails = 0;
protected $properties = array();
private $provider;
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthAuthProviderPHIDType::TYPECONST);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'isEnabled' => 'bool',
'providerClass' => 'text128',
'providerType' => 'text32',
'providerDomain' => 'text128',
'shouldAllowLogin' => 'bool',
'shouldAllowRegistration' => 'bool',
'shouldAllowLink' => 'bool',
'shouldAllowUnlink' => 'bool',
'shouldTrustEmails' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_provider' => array(
'columns' => array('providerType', 'providerDomain'),
'unique' => true,
),
'key_class' => array(
'columns' => array('providerClass'),
),
),
) + parent::getConfiguration();
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProvider() {
if (!$this->provider) {
$base = PhabricatorAuthProvider::getAllBaseProviders();
$found = null;
foreach ($base as $provider) {
if (get_class($provider) == $this->providerClass) {
$found = $provider;
break;
}
}
if ($found) {
$this->provider = id(clone $found)->attachProviderConfig($this);
}
}
return $this->provider;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthProviderConfigEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthProviderConfigTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_ADMIN;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index 81348a9bc7..d2f172fc72 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,572 +1,575 @@
<?php
abstract class PhabricatorController extends AphrontController {
private $handles;
public function shouldRequireLogin() {
return true;
}
public function shouldRequireAdmin() {
return false;
}
public function shouldRequireEnabledUser() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowPartialSessions() {
return false;
}
public function shouldRequireEmailVerification() {
return PhabricatorUserEmail::isEmailVerificationRequired();
}
public function shouldAllowRestrictedParameter($parameter_name) {
return false;
}
public function shouldRequireMultiFactorEnrollment() {
if (!$this->shouldRequireLogin()) {
return false;
}
if (!$this->shouldRequireEnabledUser()) {
return false;
}
if ($this->shouldAllowPartialSessions()) {
return false;
}
$user = $this->getRequest()->getUser();
if (!$user->getIsStandardUser()) {
return false;
}
return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
}
public function willBeginExecution() {
$request = $this->getRequest();
if ($request->getUser()) {
// NOTE: Unit tests can set a user explicitly. Normal requests are not
// permitted to do this.
PhabricatorTestCase::assertExecutingUnitTests();
$user = $request->getUser();
} else {
$user = new PhabricatorUser();
$session_engine = new PhabricatorAuthSessionEngine();
$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($phsid)) {
$session_user = $session_engine->loadUserForSession(
PhabricatorAuthSession::TYPE_WEB,
$phsid);
if ($session_user) {
$user = $session_user;
}
} else {
// If the client doesn't have a session token, generate an anonymous
// session. This is used to provide CSRF protection to logged-out users.
$phsid = $session_engine->establishSession(
PhabricatorAuthSession::TYPE_WEB,
null,
$partial = false);
// This may be a resource request, in which case we just don't set
// the cookie.
if ($request->canSetCookies()) {
$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);
}
}
if (!$user->isLoggedIn()) {
$user->attachAlternateCSRFString(PhabricatorHash::digest($phsid));
}
$request->setUser($user);
}
$translation = $user->getTranslation();
if ($translation &&
$translation != PhabricatorEnv::getEnvConfig('translation.provider')) {
$translation = newv($translation, array());
PhutilTranslator::getInstance()
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
}
$preferences = $user->loadPreferences();
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
$dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE;
if ($preferences->getPreference($dark_console) ||
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
$console = new DarkConsoleCore();
$request->getApplicationConfiguration()->setConsole($console);
}
}
// NOTE: We want to set up the user first so we can render a real page
// here, but fire this before any real logic.
$restricted = array(
'code',
);
foreach ($restricted as $parameter) {
if ($request->getExists($parameter)) {
if (!$this->shouldAllowRestrictedParameter($parameter)) {
throw new Exception(
pht(
'Request includes restricted parameter "%s", but this '.
'controller ("%s") does not whitelist it. Refusing to '.
'serve this request because it might be part of a redirection '.
'attack.',
$parameter,
get_class($this)));
}
}
}
if ($this->shouldRequireEnabledUser()) {
if ($user->isLoggedIn() && !$user->getIsApproved()) {
$controller = new PhabricatorAuthNeedsApprovalController();
return $this->delegateToController($controller);
}
if ($user->getIsDisabled()) {
$controller = new PhabricatorDisabledUserController();
return $this->delegateToController($controller);
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST,
array(
'request' => $request,
'controller' => $this,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$checker_controller = $event->getValue('controller');
if ($checker_controller != $this) {
return $this->delegateToController($checker_controller);
}
$auth_class = 'PhabricatorAuthApplication';
$auth_application = PhabricatorApplication::getByClass($auth_class);
// Require partial sessions to finish login before doing anything.
if (!$this->shouldAllowPartialSessions()) {
if ($user->hasSession() &&
$user->getSession()->getIsPartial()) {
$login_controller = new PhabricatorAuthFinishController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
}
// Check if the user needs to configure MFA.
$need_mfa = $this->shouldRequireMultiFactorEnrollment();
$have_mfa = $user->getIsEnrolledInMultiFactor();
if ($need_mfa && !$have_mfa) {
// Check if the cache is just out of date. Otherwise, roadblock the user
// and require MFA enrollment.
$user->updateMultiFactorEnrollment();
if (!$user->getIsEnrolledInMultiFactor()) {
$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($mfa_controller);
}
}
if ($this->shouldRequireLogin()) {
// This actually means we need either:
// - a valid user, or a public controller; and
// - permission to see the application.
$allow_public = $this->shouldAllowPublic() &&
PhabricatorEnv::getEnvConfig('policy.allow-public');
// If this controller isn't public, and the user isn't logged in, require
// login.
if (!$allow_public && !$user->isLoggedIn()) {
$login_controller = new PhabricatorAuthStartController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
if ($user->isLoggedIn()) {
if ($this->shouldRequireEmailVerification()) {
if (!$user->getIsEmailVerified()) {
$controller = new PhabricatorMustVerifyEmailController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($controller);
}
}
}
// If the user doesn't have access to the application, don't let them use
// any of its controllers. We query the application in order to generate
// a policy exception if the viewer doesn't have permission.
$application = $this->getCurrentApplication();
if ($application) {
id(new PhabricatorApplicationQuery())
->setViewer($user)
->withPHIDs(array($application->getPHID()))
->executeOne();
}
}
// NOTE: We do this last so that users get a login page instead of a 403
// if they need to login.
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
return new Aphront403Response();
}
}
public function buildStandardPageView() {
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->setController($this);
return $view;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
$response = new AphrontWebpageResponse();
$response->setContent($page->render());
return $response;
}
public function getApplicationURI($path = '') {
if (!$this->getCurrentApplication()) {
throw new Exception('No application!');
}
return $this->getCurrentApplication()->getApplicationURI($path);
}
public function buildApplicationPage($view, array $options) {
$page = $this->buildStandardPageView();
$title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ?
'Phabricator' :
pht('Bacon Ice Cream for Breakfast');
$application = $this->getCurrentApplication();
$page->setTitle(idx($options, 'title', $title));
if ($application) {
$page->setApplicationName($application->getName());
if ($application->getTitleGlyph()) {
$page->setGlyph($application->getTitleGlyph());
}
}
if (!($view instanceof AphrontSideNavFilterView)) {
$nav = new AphrontSideNavFilterView();
$nav->appendChild($view);
$view = $nav;
}
$user = $this->getRequest()->getUser();
$view->setUser($user);
$page->appendChild($view);
$object_phids = idx($options, 'pageObjects', array());
if ($object_phids) {
$page->appendPageObjects($object_phids);
foreach ($object_phids as $object_phid) {
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$user,
$object_phid);
}
}
if (idx($options, 'device', true)) {
$page->setDeviceReady(true);
}
$page->setShowFooter(idx($options, 'showFooter', true));
$page->setShowChrome(idx($options, 'chrome', true));
$application_menu = $this->buildApplicationMenu();
if ($application_menu) {
$page->setApplicationMenu($application_menu);
}
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
public function didProcessRequest($response) {
// If a bare DialogView is returned, wrap it in a DialogResponse.
if ($response instanceof AphrontDialogView) {
$response = id(new AphrontDialogResponse())->setDialog($response);
}
$request = $this->getRequest();
$response->setRequest($request);
$seen = array();
while ($response instanceof AphrontProxyResponse) {
$hash = spl_object_hash($response);
if (isset($seen[$hash])) {
$seen[] = get_class($response);
throw new Exception(
'Cycle while reducing proxy responses: '.
implode(' -> ', $seen));
}
$seen[$hash] = get_class($response);
$response = $response->reduceProxyResponse();
}
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax()) {
$dialog = $response->getDialog();
$title = $dialog->getTitle();
$short = $dialog->getShortTitle();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(coalesce($short, $title));
$page_content = array(
$crumbs,
$response->buildResponseString(),
);
$view = id(new PhabricatorStandardPageView())
->setRequest($request)
->setController($this)
->setDeviceReady(true)
->setTitle($title)
->appendChild($page_content);
$response = id(new AphrontWebpageResponse())
->setContent($view->render())
->setHTTPResponseCode($response->getHTTPResponseCode());
} else {
$response->getDialog()->setIsStandalone(true);
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
));
}
}
return $response;
}
protected function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
"Attempting to access handle which wasn't loaded: {$phid}");
}
return $this->handles[$phid];
}
protected function loadHandles(array $phids) {
$phids = array_filter($phids);
$this->handles = $this->loadViewerHandles($phids);
return $this;
}
protected function getLoadedHandles() {
return $this->handles;
}
protected function loadViewerHandles(array $phids) {
return id(new PhabricatorHandleQuery())
->setViewer($this->getRequest()->getUser())
->withPHIDs($phids)
->execute();
}
/**
* Render a list of links to handles, identified by PHIDs. The handles must
* already be loaded.
*
* @param list<phid> List of PHIDs to render links to.
* @param string Style, one of "\n" (to put each item on its own line)
* or "," (to list items inline, separated by commas).
* @return string Rendered list of handle links.
*/
protected function renderHandlesForPHIDs(array $phids, $style = "\n") {
$style_map = array(
"\n" => phutil_tag('br'),
',' => ', ',
);
if (empty($style_map[$style])) {
throw new Exception("Unknown handle list style '{$style}'!");
}
return implode_selected_handle_links($style_map[$style],
$this->getLoadedHandles(),
array_filter($phids));
}
protected function buildApplicationMenu() {
return null;
}
protected function buildApplicationCrumbs() {
$crumbs = array();
$application = $this->getCurrentApplication();
if ($application) {
$sprite = $application->getIconName();
if (!$sprite) {
$sprite = 'application';
}
$crumbs[] = id(new PhabricatorCrumbView())
->setHref($this->getApplicationURI())
->setAural($application->getName())
->setIcon($sprite);
}
$view = new PhabricatorCrumbsView();
foreach ($crumbs as $crumb) {
$view->addCrumb($crumb);
}
return $view;
}
protected function hasApplicationCapability($capability) {
return PhabricatorPolicyFilter::hasCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function requireApplicationCapability($capability) {
PhabricatorPolicyFilter::requireCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function explainApplicationCapability(
$capability,
$positive_message,
$negative_message) {
$can_act = $this->hasApplicationCapability($capability);
if ($can_act) {
$message = $positive_message;
$icon_name = 'fa-play-circle-o lightgreytext';
} else {
$message = $negative_message;
$icon_name = 'fa-lock';
}
$icon = id(new PHUIIconView())
->setIconFont($icon_name);
require_celerity_resource('policy-css');
$phid = $this->getCurrentApplication()->getPHID();
$explain_uri = "/policy/explain/{$phid}/{$capability}/";
$message = phutil_tag(
'div',
array(
'class' => 'policy-capability-explanation',
),
array(
$icon,
javelin_tag(
'a',
array(
'href' => $explain_uri,
'sigil' => 'workflow',
),
$message),
));
return array($can_act, $message);
}
public function getDefaultResourceSource() {
return 'phabricator';
}
/**
* Create a new @{class:AphrontDialogView} with defaults filled in.
*
* @return AphrontDialogView New dialog.
*/
public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath();
return id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setSubmitURI($submit_uri);
}
protected function buildTransactionTimeline(
PhabricatorApplicationTransactionInterface $object,
PhabricatorApplicationTransactionQuery $query,
- PhabricatorMarkupEngine $engine = null) {
+ PhabricatorMarkupEngine $engine = null,
+ $render_data = array()) {
$viewer = $this->getRequest()->getUser();
$xaction = $object->getApplicationTransactionTemplate();
$view = $xaction->getApplicationTransactionViewObject();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($this->getRequest())
->setURI(new PhutilURI(
'/transactions/showolder/'.$object->getPHID().'/'));
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->needComments(true)
->setReversePaging(false)
->executeWithCursorPager($pager);
$xactions = array_reverse($xactions);
if ($engine) {
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$view->setMarkupEngine($engine);
}
$timeline = $view
->setUser($viewer)
->setObjectPHID($object->getPHID())
->setTransactions($xactions)
- ->setPager($pager);
+ ->setPager($pager)
+ ->setRenderData($render_data);
+ $object->willRenderTimeline($timeline, $this->getRequest());
return $timeline;
}
}
diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php
index 736b4facac..54fac1e333 100644
--- a/src/applications/config/storage/PhabricatorConfigEntry.php
+++ b/src/applications/config/storage/PhabricatorConfigEntry.php
@@ -1,94 +1,101 @@
<?php
final class PhabricatorConfigEntry
extends PhabricatorConfigEntryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $namespace;
protected $configKey;
protected $value;
protected $isDeleted;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'value' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'namespace' => 'text64',
'configKey' => 'text64',
'isDeleted' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('namespace', 'configKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorConfigConfigPHIDType::TYPECONST);
}
public static function loadConfigEntry($key) {
$config_entry = id(new PhabricatorConfigEntry())
->loadOneWhere(
'configKey = %s AND namespace = %s',
$key,
'default');
if (!$config_entry) {
$config_entry = id(new PhabricatorConfigEntry())
->setConfigKey($key)
->setNamespace('default');
}
return $config_entry;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorConfigEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorConfigTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_ADMIN;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php
index 1dd06b851a..2b5dd29d53 100644
--- a/src/applications/dashboard/storage/PhabricatorDashboard.php
+++ b/src/applications/dashboard/storage/PhabricatorDashboard.php
@@ -1,150 +1,157 @@
<?php
/**
* A collection of dashboard panels with a specific layout.
*/
final class PhabricatorDashboard extends PhabricatorDashboardDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $viewPolicy;
protected $editPolicy;
protected $layoutConfig = array();
private $panelPHIDs = self::ATTACHABLE;
private $panels = self::ATTACHABLE;
public static function initializeNewDashboard(PhabricatorUser $actor) {
return id(new PhabricatorDashboard())
->setName('')
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy($actor->getPHID())
->attachPanels(array())
->attachPanelPHIDs(array());
}
public static function copyDashboard(
PhabricatorDashboard $dst,
PhabricatorDashboard $src) {
$dst->name = $src->name;
$dst->layoutConfig = $src->layoutConfig;
return $dst;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'layoutConfig' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorDashboardDashboardPHIDType::TYPECONST);
}
public function getLayoutConfigObject() {
return PhabricatorDashboardLayoutConfig::newFromDictionary(
$this->getLayoutConfig());
}
public function setLayoutConfigFromObject(
PhabricatorDashboardLayoutConfig $object) {
$this->setLayoutConfig($object->toDictionary());
return $this;
}
public function attachPanelPHIDs(array $phids) {
$this->panelPHIDs = $phids;
return $this;
}
public function getPanelPHIDs() {
return $this->assertAttached($this->panelPHIDs);
}
public function attachPanels(array $panels) {
assert_instances_of($panels, 'PhabricatorDashboardPanel');
$this->panels = $panels;
return $this;
}
public function getPanels() {
return $this->assertAttached($this->panels);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorDashboardTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$installs = id(new PhabricatorDashboardInstall())->loadAllWhere(
'dashboardPHID = %s',
$this->getPHID());
foreach ($installs as $install) {
$install->delete();
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
index a18c1c8f98..bf86cb2ac0 100644
--- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
+++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
@@ -1,169 +1,176 @@
<?php
/**
* An individual dashboard panel.
*/
final class PhabricatorDashboardPanel
extends PhabricatorDashboardDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $panelType;
protected $viewPolicy;
protected $editPolicy;
protected $isArchived = 0;
protected $properties = array();
private $customFields = self::ATTACHABLE;
public static function initializeNewPanel(PhabricatorUser $actor) {
return id(new PhabricatorDashboardPanel())
->setName('')
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy($actor->getPHID());
}
public static function copyPanel(
PhabricatorDashboardPanel $dst,
PhabricatorDashboardPanel $src) {
$dst->name = $src->name;
$dst->panelType = $src->panelType;
$dst->properties = $src->properties;
return $dst;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'panelType' => 'text64',
'isArchived' => 'bool',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorDashboardPanelPHIDType::TYPECONST);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getMonogram() {
return 'W'.$this->getID();
}
public function getImplementation() {
return idx(
PhabricatorDashboardPanelType::getAllPanelTypes(),
$this->getPanelType());
}
public function requireImplementation() {
$impl = $this->getImplementation();
if (!$impl) {
throw new Exception(
pht(
'Attempting to use a panel in a way that requires an '.
'implementation, but the panel implementation ("%s") is unknown to '.
'Phabricator.',
$this->getPanelType()));
}
return $impl;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorDashboardPanelTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardPanelTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'PhabricatorDashboardPanelCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index 85faa55ce7..21e9c7ffc2 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1,960 +1,960 @@
<?php
final class DifferentialRevisionViewController extends DifferentialController {
private $revisionID;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_is_anonymous = !$user->isLoggedIn();
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($this->revisionID))
->setViewer($request->getUser())
->needRelationships(true)
->needReviewerStatus(true)
->needReviewerAuthority(true)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withRevisionIDs(array($this->revisionID))
->needArcanistProjects(true)
->execute();
$diffs = array_reverse($diffs, $preserve_keys = true);
if (!$diffs) {
throw new Exception(
'This revision has no diffs. Something has gone quite wrong.');
}
$revision->attachActiveDiff(last($diffs));
$diff_vs = $request->getInt('vs');
$target_id = $request->getInt('id');
$target = idx($diffs, $target_id, end($diffs));
$target_manual = $target;
if (!$target_id) {
foreach ($diffs as $diff) {
if ($diff->getCreationMethod() != 'commit') {
$target_manual = $diff;
}
}
}
if (empty($diffs[$diff_vs])) {
$diff_vs = null;
}
$repository = null;
$repository_phid = $target->getRepositoryPHID();
if ($repository_phid) {
if ($repository_phid == $revision->getRepositoryPHID()) {
$repository = $revision->getRepository();
} else {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->withPHIDs(array($repository_phid))
->executeOne();
}
}
list($changesets, $vs_map, $vs_changesets, $rendering_references) =
$this->loadChangesetsAndVsMap(
$target,
idx($diffs, $diff_vs),
$repository);
if ($request->getExists('download')) {
return $this->buildRawDiffResponse(
$revision,
$changesets,
$vs_changesets,
$vs_map,
$repository);
}
$props = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$target_manual->getID());
$props = mpull($props, 'getData', 'getName');
$all_changesets = $changesets;
$inlines = $this->loadInlineComments(
$revision,
$all_changesets);
$object_phids = array_merge(
$revision->getReviewers(),
$revision->getCCPHIDs(),
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$user->getPHID(),
));
foreach ($revision->getAttached() as $type => $phids) {
foreach ($phids as $phid => $info) {
$object_phids[] = $phid;
}
}
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_VIEW);
$field_list->setViewer($user);
$field_list->readFieldsFromStorage($revision);
$warning_handle_map = array();
foreach ($field_list->getFields() as $key => $field) {
$req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings();
foreach ($req as $phid) {
$warning_handle_map[$key][] = $phid;
$object_phids[] = $phid;
}
}
$handles = $this->loadViewerHandles($object_phids);
$request_uri = $request->getRequestURI();
$limit = 100;
$large = $request->getStr('large');
if (count($changesets) > $limit && !$large) {
$count = count($changesets);
$warning = new AphrontErrorView();
$warning->setTitle('Very Large Diff');
$warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$warning->appendChild(hsprintf(
'%s <strong>%s</strong>',
pht(
'This diff is very large and affects %s files. '.
'You may load each file individually or ',
new PhutilNumber($count)),
phutil_tag(
'a',
array(
'class' => 'button grey',
'href' => $request_uri
->alter('large', 'true')
->setFragment('toc'),
),
pht('Show All Files Inline'))));
$warning = $warning->render();
$my_inlines = id(new DifferentialInlineCommentQuery())
->withDraftComments($user->getPHID(), $this->revisionID)
->execute();
$visible_changesets = array();
foreach ($inlines + $my_inlines as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
$visible_changesets[$changeset_id] = $changesets[$changeset_id];
}
}
if (!empty($props['arc:lint'])) {
$changeset_paths = mpull($changesets, null, 'getFilename');
foreach ($props['arc:lint'] as $lint) {
$changeset = idx($changeset_paths, $lint['path']);
if ($changeset) {
$visible_changesets[$changeset->getID()] = $changeset;
}
}
}
} else {
$warning = null;
$visible_changesets = $changesets;
}
// TODO: This should be in a DiffQuery or similar.
$need_props = array();
foreach ($field_list->getFields() as $field) {
foreach ($field->getRequiredDiffPropertiesForRevisionView() as $prop) {
$need_props[$prop] = $prop;
}
}
if ($need_props) {
$prop_diff = $revision->getActiveDiff();
$load_props = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d AND name IN (%Ls)',
$prop_diff->getID(),
$need_props);
$load_props = mpull($load_props, 'getData', 'getName');
foreach ($need_props as $need) {
$prop_diff->attachProperty($need, idx($load_props, $need));
}
}
$commit_hashes = mpull($diffs, 'getSourceControlBaseRevision');
$local_commits = idx($props, 'local:commits', array());
foreach ($local_commits as $local_commit) {
$commit_hashes[] = idx($local_commit, 'tree');
$commit_hashes[] = idx($local_commit, 'local');
}
$commit_hashes = array_unique(array_filter($commit_hashes));
if ($commit_hashes) {
$commits_for_links = id(new DiffusionCommitQuery())
->setViewer($user)
->withIdentifiers($commit_hashes)
->execute();
$commits_for_links = mpull(
$commits_for_links,
null,
'getCommitIdentifier');
} else {
$commits_for_links = array();
}
$revision_detail = id(new DifferentialRevisionDetailView())
->setUser($user)
->setRevision($revision)
->setDiff(end($diffs))
->setCustomFields($field_list)
->setURI($request->getRequestURI());
$actions = $this->getRevisionActions($revision);
$whitespace = $request->getStr(
'whitespace',
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL);
$arc_project = $target->getArcanistProject();
if ($arc_project) {
list($symbol_indexes, $project_phids) = $this->buildSymbolIndexes(
$arc_project,
$visible_changesets);
} else {
$symbol_indexes = array();
$project_phids = null;
}
$revision_detail->setActions($actions);
$revision_detail->setUser($user);
$revision_detail_box = $revision_detail->render();
$revision_warnings = $this->buildRevisionWarnings(
$revision,
$field_list,
$warning_handle_map,
$handles);
if ($revision_warnings) {
$revision_warnings = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setErrors($revision_warnings);
$revision_detail_box->setErrorView($revision_warnings);
}
$comment_view = $this->buildTransactions(
$revision,
$diff_vs ? $diffs[$diff_vs] : $target,
$target,
$all_changesets);
if (!$viewer_is_anonymous) {
$comment_view->setQuoteRef('D'.$revision->getID());
$comment_view->setQuoteTargetID('comment-content');
}
$wrap_id = celerity_generate_unique_node_id();
$comment_view = phutil_tag(
'div',
array(
'id' => $wrap_id,
),
$comment_view);
if ($arc_project) {
Javelin::initBehavior(
'repository-crossreference',
array(
'section' => $wrap_id,
'projects' => $project_phids,
));
}
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setChangesets($changesets);
$changeset_view->setVisibleChangesets($visible_changesets);
if (!$viewer_is_anonymous) {
$changeset_view->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision->getID().'/');
}
$changeset_view->setStandaloneURI('/differential/changeset/');
$changeset_view->setRawFileURIs(
'/differential/changeset/?view=old',
'/differential/changeset/?view=new');
$changeset_view->setUser($user);
$changeset_view->setDiff($target);
$changeset_view->setRenderingReferences($rendering_references);
$changeset_view->setVsMap($vs_map);
$changeset_view->setWhitespace($whitespace);
if ($repository) {
$changeset_view->setRepository($repository);
}
$changeset_view->setSymbolIndexes($symbol_indexes);
$changeset_view->setTitle('Diff '.$target->getID());
$diff_history = id(new DifferentialRevisionUpdateHistoryView())
->setUser($user)
->setDiffs($diffs)
->setSelectedVersusDiffID($diff_vs)
->setSelectedDiffID($target->getID())
->setSelectedWhitespace($whitespace)
->setCommitsForLinks($commits_for_links);
$local_view = id(new DifferentialLocalCommitsView())
->setUser($user)
->setLocalCommits(idx($props, 'local:commits'))
->setCommitsForLinks($commits_for_links);
if ($repository) {
$other_revisions = $this->loadOtherRevisions(
$changesets,
$target,
$repository);
} else {
$other_revisions = array();
}
$other_view = null;
if ($other_revisions) {
$other_view = $this->renderOtherRevisions($other_revisions);
}
$toc_view = new DifferentialDiffTableOfContentsView();
$toc_view->setChangesets($changesets);
$toc_view->setVisibleChangesets($visible_changesets);
$toc_view->setRenderingReferences($rendering_references);
$toc_view->setUnitTestData(idx($props, 'arc:unit', array()));
if ($repository) {
$toc_view->setRepository($repository);
}
$toc_view->setDiff($target);
$toc_view->setUser($user);
$toc_view->setRevisionID($revision->getID());
$toc_view->setWhitespace($whitespace);
$comment_form = null;
if (!$viewer_is_anonymous) {
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'differential-comment-'.$revision->getID());
$reviewers = array();
$ccs = array();
if ($draft) {
$reviewers = idx($draft->getMetadata(), 'reviewers', array());
$ccs = idx($draft->getMetadata(), 'ccs', array());
if ($reviewers || $ccs) {
$handles = $this->loadViewerHandles(array_merge($reviewers, $ccs));
$reviewers = array_select_keys($handles, $reviewers);
$ccs = array_select_keys($handles, $ccs);
}
}
$comment_form = new DifferentialAddCommentView();
$comment_form->setRevision($revision);
$review_warnings = array();
foreach ($field_list->getFields() as $field) {
$review_warnings[] = $field->getWarningsForDetailView();
}
$review_warnings = array_mergev($review_warnings);
if ($review_warnings) {
$review_warnings_panel = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setErrors($review_warnings);
$comment_form->setErrorView($review_warnings_panel);
}
$comment_form->setActions($this->getRevisionCommentActions($revision));
$action_uri = $this->getApplicationURI(
'comment/save/'.$revision->getID().'/');
$comment_form->setActionURI($action_uri);
$comment_form->setUser($user);
$comment_form->setDraft($draft);
$comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID'));
$comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID'));
// TODO: This just makes the "Z" key work. Generalize this and remove
// it at some point.
$comment_form = phutil_tag(
'div',
array(
'class' => 'differential-add-comment-panel',
),
$comment_form);
}
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
Javelin::initBehavior('differential-user-select');
$page_pane = id(new DifferentialPrimaryPaneView())
->setID($pane_id)
->appendChild(array(
$comment_view,
$diff_history,
$warning,
$local_view,
$toc_view,
$other_view,
$changeset_view,
));
if ($comment_form) {
$page_pane->appendChild($comment_form);
} else {
// TODO: For now, just use this to get "Login to Comment".
$page_pane->appendChild(
id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setRequestURI($request->getRequestURI()));
}
$object_id = 'D'.$revision->getID();
$top_anchor = id(new PhabricatorAnchorView())
->setAnchorName('top')
->setNavigationMarker(true);
$content = array(
$top_anchor,
$revision_detail_box,
$page_pane,
);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($object_id, '/'.$object_id);
$crumbs->setActionList($revision_detail->getActionList());
$prefs = $user->loadPreferences();
$pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE;
if ($prefs->getPreference($pref_filetree)) {
$collapsed = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED,
false);
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setAnchorName('top')
->setTitle('D'.$revision->getID())
->setBaseURI(new PhutilURI('/D'.$revision->getID()))
->setCollapsed((bool)$collapsed)
->build($changesets);
$nav->appendChild($content);
$nav->setCrumbs($crumbs);
$content = $nav;
} else {
array_unshift($content, $crumbs);
}
return $this->buildApplicationPage(
$content,
array(
'title' => $object_id.' '.$revision->getTitle(),
'pageObjects' => array($revision->getPHID()),
));
}
private function getRevisionActions(DifferentialRevision $revision) {
$viewer = $this->getRequest()->getUser();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$revision,
PhabricatorPolicyCapability::CAN_EDIT);
$actions = array();
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref("/differential/revision/edit/{$revision_id}/")
->setName(pht('Edit Revision'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$this->requireResource('phabricator-object-selector-css');
$this->requireResource('javelin-behavior-phabricator-object-selector');
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-link')
->setName(pht('Edit Dependencies'))
->setHref("/search/attach/{$revision_phid}/DREV/dependencies/")
->setWorkflow(true)
->setDisabled(!$can_edit);
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-anchor')
->setName(pht('Edit Maniphest Tasks'))
->setHref("/search/attach/{$revision_phid}/TASK/")
->setWorkflow(true)
->setDisabled(!$can_edit);
}
$request_uri = $this->getRequest()->getRequestURI();
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Download Raw Diff'))
->setHref($request_uri->alter('download', 'true'));
return $actions;
}
private function getRevisionCommentActions(DifferentialRevision $revision) {
$actions = array(
DifferentialAction::ACTION_COMMENT => true,
);
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$status = $revision->getStatus();
$viewer_has_accepted = false;
$viewer_has_rejected = false;
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
foreach ($revision->getReviewerStatus() as $reviewer) {
if ($reviewer->getReviewerPHID() == $viewer_phid) {
if ($reviewer->getStatus() == $status_accepted) {
$viewer_has_accepted = true;
}
if ($reviewer->getStatus() == $status_rejected) {
$viewer_has_rejected = true;
}
break;
}
}
$allow_self_accept = PhabricatorEnv::getEnvConfig(
'differential.allow-self-accept');
$always_allow_abandon = PhabricatorEnv::getEnvConfig(
'differential.always-allow-abandon');
$always_allow_close = PhabricatorEnv::getEnvConfig(
'differential.always-allow-close');
$allow_reopen = PhabricatorEnv::getEnvConfig(
'differential.allow-reopen');
if ($viewer_is_owner) {
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept;
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
$actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept;
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
break;
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
$actions[DifferentialAction::ACTION_CLOSE] = true;
break;
case ArcanistDifferentialRevisionStatus::CLOSED:
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
$actions[DifferentialAction::ACTION_RECLAIM] = true;
break;
}
} else {
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ABANDON] = $always_allow_abandon;
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
$actions[DifferentialAction::ACTION_ABANDON] = $always_allow_abandon;
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = !$viewer_has_rejected;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = $always_allow_abandon;
$actions[DifferentialAction::ACTION_ACCEPT] = !$viewer_has_accepted;
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::CLOSED:
case ArcanistDifferentialRevisionStatus::ABANDONED:
break;
}
if ($status != ArcanistDifferentialRevisionStatus::CLOSED) {
$actions[DifferentialAction::ACTION_CLAIM] = true;
$actions[DifferentialAction::ACTION_CLOSE] = $always_allow_close;
}
}
$actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
$actions[DifferentialAction::ACTION_ADDCCS] = true;
$actions[DifferentialAction::ACTION_REOPEN] = $allow_reopen &&
($status == ArcanistDifferentialRevisionStatus::CLOSED);
$actions = array_keys(array_filter($actions));
$actions_dict = array();
foreach ($actions as $action) {
$actions_dict[$action] = DifferentialAction::getActionVerb($action);
}
return $actions_dict;
}
private function loadInlineComments(
DifferentialRevision $revision,
array &$changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$inline_comments = array();
$inline_comments = id(new DifferentialInlineCommentQuery())
->withRevisionIDs(array($revision->getID()))
->withNotDraft(true)
->execute();
$load_changesets = array();
foreach ($inline_comments as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
continue;
}
$load_changesets[$changeset_id] = true;
}
$more_changesets = array();
if ($load_changesets) {
$changeset_ids = array_keys($load_changesets);
$more_changesets += id(new DifferentialChangeset())
->loadAllWhere(
'id IN (%Ld)',
$changeset_ids);
}
if ($more_changesets) {
$changesets += $more_changesets;
$changesets = msort($changesets, 'getSortKey');
}
return $inline_comments;
}
private function loadChangesetsAndVsMap(
DifferentialDiff $target,
DifferentialDiff $diff_vs = null,
PhabricatorRepository $repository = null) {
$load_diffs = array($target);
if ($diff_vs) {
$load_diffs[] = $diff_vs;
}
$raw_changesets = id(new DifferentialChangesetQuery())
->setViewer($this->getRequest()->getUser())
->withDiffs($load_diffs)
->execute();
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
$refs = array();
$vs_map = array();
$vs_changesets = array();
if ($diff_vs) {
$vs_id = $diff_vs->getID();
$vs_changesets_path_map = array();
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
$vs_changesets_path_map[$path] = $changeset;
$vs_changesets[$changeset->getID()] = $changeset;
}
foreach ($changesets as $key => $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
if (isset($vs_changesets_path_map[$path])) {
$vs_map[$changeset->getID()] =
$vs_changesets_path_map[$path]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
unset($vs_changesets_path_map[$path]);
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
foreach ($vs_changesets_path_map as $path => $changeset) {
$changesets[$changeset->getID()] = $changeset;
$vs_map[$changeset->getID()] = -1;
$refs[$changeset->getID()] = $changeset->getID().'/-1';
}
} else {
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
}
$changesets = msort($changesets, 'getSortKey');
return array($changesets, $vs_map, $vs_changesets, $refs);
}
private function buildSymbolIndexes(
PhabricatorRepositoryArcanistProject $arc_project,
array $visible_changesets) {
assert_instances_of($visible_changesets, 'DifferentialChangeset');
$engine = PhabricatorSyntaxHighlighter::newEngine();
$langs = $arc_project->getSymbolIndexLanguages();
if (!$langs) {
return array(array(), array());
}
$symbol_indexes = array();
$project_phids = array_merge(
array($arc_project->getPHID()),
nonempty($arc_project->getSymbolIndexProjects(), array()));
$indexed_langs = array_fill_keys($langs, true);
foreach ($visible_changesets as $key => $changeset) {
$lang = $engine->getLanguageFromFilename($changeset->getFilename());
if (isset($indexed_langs[$lang])) {
$symbol_indexes[$key] = array(
'lang' => $lang,
'projects' => $project_phids,
);
}
}
return array($symbol_indexes, $project_phids);
}
private function loadOtherRevisions(
array $changesets,
DifferentialDiff $target,
PhabricatorRepository $repository) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $changeset->getAbsoluteRepositoryPath(
$repository,
$target);
}
if (!$paths) {
return array();
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (!$path_map) {
return array();
}
$query = id(new DifferentialRevisionQuery())
->setViewer($this->getRequest()->getUser())
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED)
->setLimit(10)
->needFlags(true)
->needDrafts(true)
->needRelationships(true);
foreach ($path_map as $path => $path_id) {
$query->withPath($repository->getID(), $path_id);
}
$results = $query->execute();
// Strip out *this* revision.
foreach ($results as $key => $result) {
if ($result->getID() == $this->revisionID) {
unset($results[$key]);
}
}
return $results;
}
private function renderOtherRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$user = $this->getRequest()->getUser();
$view = id(new DifferentialRevisionListView())
->setRevisions($revisions)
->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Open Revisions Affecting These Files'))
->appendChild($view);
}
/**
* Note this code is somewhat similar to the buildPatch method in
* @{class:DifferentialReviewRequestMail}.
*
* @return @{class:AphrontRedirectResponse}
*/
private function buildRawDiffResponse(
DifferentialRevision $revision,
array $changesets,
array $vs_changesets,
array $vs_map,
PhabricatorRepository $repository = null) {
assert_instances_of($changesets, 'DifferentialChangeset');
assert_instances_of($vs_changesets, 'DifferentialChangeset');
$viewer = $this->getRequest()->getUser();
id(new DifferentialHunkQuery())
->setViewer($viewer)
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
$diff = new DifferentialDiff();
$diff->attachChangesets($changesets);
$raw_changes = $diff->buildChangesList();
$changes = array();
foreach ($raw_changes as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$loader = id(new PhabricatorFileBundleLoader())
->setViewer($viewer);
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
$vcs = $repository ? $repository->getVersionControlSystem() : null;
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$raw_diff = $bundle->toGitPatch();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
default:
$raw_diff = $bundle->toUnifiedDiff();
break;
}
$request_uri = $this->getRequest()->getRequestURI();
// this ends up being something like
// D123.diff
// or the verbose
// D123.vs123.id123.whitespaceignore-all.diff
// lame but nice to include these options
$file_name = ltrim($request_uri->getPath(), '/').'.';
foreach ($request_uri->getQueryParams() as $key => $value) {
if ($key == 'download') {
continue;
}
$file_name .= $key.$value.'.';
}
$file_name .= 'diff';
$file = PhabricatorFile::buildFromFileDataOrHash(
$raw_diff,
array(
'name' => $file_name,
'ttl' => (60 * 60 * 24),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file->attachToObject($revision->getPHID());
unset($unguarded);
return $file->getRedirectResponse();
}
private function buildTransactions(
DifferentialRevision $revision,
DifferentialDiff $left_diff,
DifferentialDiff $right_diff,
array $changesets) {
$timeline = $this->buildTransactionTimeline(
$revision,
- new DifferentialTransactionQuery());
- $timeline
- ->setChangesets($changesets)
- ->setRevision($revision)
- ->setLeftDiff($left_diff)
- ->setRightDiff($right_diff);
+ new DifferentialTransactionQuery(),
+ $engine = null,
+ array(
+ 'left' => $left_diff->getID(),
+ 'right' => $right_diff->getID(),
+ ));
return $timeline;
}
private function buildRevisionWarnings(
DifferentialRevision $revision,
PhabricatorCustomFieldList $field_list,
array $warning_handle_map,
array $handles) {
$warnings = array();
foreach ($field_list->getFields() as $key => $field) {
$phids = idx($warning_handle_map, $key, array());
$field_handles = array_select_keys($handles, $phids);
$field_warnings = $field->getWarningsForRevisionHeader($field_handles);
foreach ($field_warnings as $warning) {
$warnings[] = $warning;
}
}
return $warnings;
}
}
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
index abb677287f..f055000fdb 100644
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -1,461 +1,466 @@
<?php
final class DifferentialDiff
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
HarbormasterBuildableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $revisionID;
protected $authorPHID;
protected $repositoryPHID;
protected $sourceMachine;
protected $sourcePath;
protected $sourceControlSystem;
protected $sourceControlBaseRevision;
protected $sourceControlPath;
protected $lintStatus;
protected $unitStatus;
protected $lineCount;
protected $branch;
protected $bookmark;
protected $arcanistProjectPHID;
protected $creationMethod;
protected $repositoryUUID;
protected $description;
protected $viewPolicy;
private $unsavedChangesets = array();
private $changesets = self::ATTACHABLE;
private $arcanistProject = self::ATTACHABLE;
private $revision = self::ATTACHABLE;
private $properties = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'revisionID' => 'id?',
'authorPHID' => 'phid?',
'repositoryPHID' => 'phid?',
'sourceMachine' => 'text255?',
'sourcePath' => 'text255?',
'sourceControlSystem' => 'text64?',
'sourceControlBaseRevision' => 'text255?',
'sourceControlPath' => 'text255?',
'lintStatus' => 'uint32',
'unitStatus' => 'uint32',
'lineCount' => 'uint32',
'branch' => 'text255?',
'bookmark' => 'text255?',
'arcanistProjectPHID' => 'phid?',
'repositoryUUID' => 'text64?',
// T6203/NULLABILITY
// These should be non-null; all diffs should have a creation method
// and the description should just be empty.
'creationMethod' => 'text255?',
'description' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'revisionID' => array(
'columns' => array('revisionID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialDiffPHIDType::TYPECONST);
}
public function addUnsavedChangeset(DifferentialChangeset $changeset) {
if ($this->changesets === null) {
$this->changesets = array();
}
$this->unsavedChangesets[] = $changeset;
$this->changesets[] = $changeset;
return $this;
}
public function attachChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->assertAttached($this->changesets);
}
public function loadChangesets() {
if (!$this->getID()) {
return array();
}
return id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$this->getID());
}
public function attachArcanistProject(
PhabricatorRepositoryArcanistProject $project = null) {
$this->arcanistProject = $project;
return $this;
}
public function getArcanistProject() {
return $this->assertAttached($this->arcanistProject);
}
public function getArcanistProjectName() {
$name = '';
if ($this->arcanistProject) {
$project = $this->getArcanistProject();
$name = $project->getName();
}
return $name;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedChangesets as $changeset) {
$changeset->setDiffID($this->getID());
$changeset->save();
}
$this->saveTransaction();
return $ret;
}
public static function initializeNewDiff(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
$diff = id(new DifferentialDiff())
->setViewPolicy($view_policy);
return $diff;
}
public static function newFromRawChanges(
PhabricatorUser $actor,
array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = self::initializeNewDiff($actor);
// There may not be any changes; initialize the changesets list so that
// we don't throw later when accessing it.
$diff->attachChangesets(array());
$lines = 0;
foreach ($changes as $change) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
// If a user pastes a diff into Differential which includes a commit
// message (e.g., they ran `git show` to generate it), discard that
// change when constructing a DifferentialDiff.
continue;
}
$changeset = new DifferentialChangeset();
$add_lines = 0;
$del_lines = 0;
$first_line = PHP_INT_MAX;
$hunks = $change->getHunks();
if ($hunks) {
foreach ($hunks as $hunk) {
$dhunk = new DifferentialHunkModern();
$dhunk->setOldOffset($hunk->getOldOffset());
$dhunk->setOldLen($hunk->getOldLength());
$dhunk->setNewOffset($hunk->getNewOffset());
$dhunk->setNewLen($hunk->getNewLength());
$dhunk->setChanges($hunk->getCorpus());
$changeset->addUnsavedHunk($dhunk);
$add_lines += $hunk->getAddLines();
$del_lines += $hunk->getDelLines();
$added_lines = $hunk->getChangedLines('new');
if ($added_lines) {
$first_line = min($first_line, head_key($added_lines));
}
}
$lines += $add_lines + $del_lines;
} else {
// This happens when you add empty files.
$changeset->attachHunks(array());
}
$metadata = $change->getAllMetadata();
if ($first_line != PHP_INT_MAX) {
$metadata['line:first'] = $first_line;
}
$changeset->setOldFile($change->getOldPath());
$changeset->setFilename($change->getCurrentPath());
$changeset->setChangeType($change->getType());
$changeset->setFileType($change->getFileType());
$changeset->setMetadata($metadata);
$changeset->setOldProperties($change->getOldProperties());
$changeset->setNewProperties($change->getNewProperties());
$changeset->setAwayPaths($change->getAwayPaths());
$changeset->setAddLines($add_lines);
$changeset->setDelLines($del_lines);
$diff->addUnsavedChangeset($changeset);
}
$diff->setLineCount($lines);
$parser = new DifferentialChangesetParser();
$changesets = $parser->detectCopiedCode(
$diff->getChangesets(),
$min_width = 30,
$min_lines = 3);
$diff->attachChangesets($changesets);
return $diff;
}
public function getDiffDict() {
$dict = array(
'id' => $this->getID(),
'revisionID' => $this->getRevisionID(),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
'sourceControlPath' => $this->getSourceControlPath(),
'sourceControlSystem' => $this->getSourceControlSystem(),
'branch' => $this->getBranch(),
'bookmark' => $this->getBookmark(),
'creationMethod' => $this->getCreationMethod(),
'description' => $this->getDescription(),
'unitStatus' => $this->getUnitStatus(),
'lintStatus' => $this->getLintStatus(),
'changes' => array(),
'properties' => array(),
'projectName' => $this->getArcanistProjectName(),
);
$dict['changes'] = $this->buildChangesList();
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $property) {
$dict['properties'][$property->getName()] = $property->getData();
if ($property->getName() == 'local:commits') {
foreach ($property->getData() as $commit) {
$dict['authorName'] = $commit['author'];
$dict['authorEmail'] = idx($commit, 'authorEmail');
break;
}
}
}
return $dict;
}
public function buildChangesList() {
$changes = array();
foreach ($this->getChangesets() as $changeset) {
$hunks = array();
foreach ($changeset->getHunks() as $hunk) {
$hunks[] = array(
'oldOffset' => $hunk->getOldOffset(),
'newOffset' => $hunk->getNewOffset(),
'oldLength' => $hunk->getOldLen(),
'newLength' => $hunk->getNewLen(),
'addLines' => null,
'delLines' => null,
'isMissingOldNewline' => null,
'isMissingNewNewline' => null,
'corpus' => $hunk->getChanges(),
);
}
$change = array(
'id' => $changeset->getID(),
'metadata' => $changeset->getMetadata(),
'oldPath' => $changeset->getOldFile(),
'currentPath' => $changeset->getFilename(),
'awayPaths' => $changeset->getAwayPaths(),
'oldProperties' => $changeset->getOldProperties(),
'newProperties' => $changeset->getNewProperties(),
'type' => $changeset->getChangeType(),
'fileType' => $changeset->getFileType(),
'commitHash' => null,
'addLines' => $changeset->getAddLines(),
'delLines' => $changeset->getDelLines(),
'hunks' => $hunks,
);
$changes[] = $change;
}
return $changes;
}
public function hasRevision() {
return $this->revision !== self::ATTACHABLE;
}
public function getRevision() {
return $this->assertAttached($this->revision);
}
public function attachRevision(DifferentialRevision $revision = null) {
$this->revision = $revision;
return $this;
}
public function attachProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key) {
return $this->assertAttachedKey($this->properties, $key);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
if ($this->hasRevision()) {
return $this->getRevision()->getPolicy($capability);
}
return $this->viewPolicy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->hasRevision()) {
return $this->getRevision()->hasAutomaticCapability($capability, $viewer);
}
return ($this->getAuthorPHID() == $viewer->getPhid());
}
public function describeAutomaticCapability($capability) {
if ($this->hasRevision()) {
return pht(
'This diff is attached to a revision, and inherits its policies.');
}
return pht('The author of a diff can see it.');
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildablePHID() {
return $this->getPHID();
}
public function getHarbormasterContainerPHID() {
if ($this->getRevisionID()) {
$revision = id(new DifferentialRevision())->load($this->getRevisionID());
if ($revision) {
return $revision->getPHID();
}
}
return null;
}
public function getBuildVariables() {
$results = array();
$results['buildable.diff'] = $this->getID();
$revision = $this->getRevision();
$results['buildable.revision'] = $revision->getID();
$repo = $revision->getRepository();
if ($repo) {
$results['repository.callsign'] = $repo->getCallsign();
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
}
return $results;
}
public function getAvailableBuildVariables() {
return array(
'buildable.diff' =>
pht('The differential diff ID, if applicable.'),
'buildable.revision' =>
pht('The differential revision ID, if applicable.'),
'repository.callsign' =>
pht('The callsign of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
-
return new DifferentialDiffEditor();
}
-
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialDiffTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
foreach ($this->loadChangesets() as $changeset) {
$changeset->delete();
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $prop) {
$prop->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index f33ba47526..22cf0d2b3c 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,527 +1,556 @@
<?php
final class DifferentialRevision extends DifferentialDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
HarbormasterBuildableInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface {
protected $title = '';
protected $originalTitle;
protected $status;
protected $summary = '';
protected $testPlan = '';
protected $authorPHID;
protected $lastReviewerPHID;
protected $lineCount = 0;
protected $attached = array();
protected $mailKey;
protected $branchName;
protected $arcanistProjectPHID;
protected $repositoryPHID;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
private $relationships = self::ATTACHABLE;
private $commits = self::ATTACHABLE;
private $activeDiff = self::ATTACHABLE;
private $diffIDs = self::ATTACHABLE;
private $hashes = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $reviewerStatus = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
public static function initializeNewRevision(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
return id(new DifferentialRevision())
->setViewPolicy($view_policy)
->setAuthorPHID($actor->getPHID())
->attachRelationships(array())
->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'originalTitle' => 'text255',
'status' => 'text32',
'summary' => 'text',
'testPlan' => 'text',
'authorPHID' => 'phid?',
'lastReviewerPHID' => 'phid?',
'lineCount' => 'uint32?',
'mailKey' => 'bytes40',
'branchName' => 'text255?',
'arcanistProjectPHID' => 'phid?',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'repositoryPHID' => array(
'columns' => array('repositoryPHID'),
),
),
) + parent::getConfiguration();
}
public function getMonogram() {
$id = $this->getID();
return "D{$id}";
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function loadRelationships() {
if (!$this->getID()) {
$this->relationships = array();
return;
}
$data = array();
$subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER);
$subscriber_phids = array_reverse($subscriber_phids);
foreach ($subscriber_phids as $phid) {
$data[] = array(
'relation' => self::RELATION_SUBSCRIBED,
'objectPHID' => $phid,
'reasonPHID' => null,
);
}
$reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER);
$reviewer_phids = array_reverse($reviewer_phids);
foreach ($reviewer_phids as $phid) {
$data[] = array(
'relation' => self::RELATION_REVIEWER,
'objectPHID' => $phid,
'reasonPHID' => null,
);
}
return $this->attachRelationships($data);
}
public function attachRelationships(array $relationships) {
$this->relationships = igroup($relationships, 'relation');
return $this;
}
public function getReviewers() {
return $this->getRelatedPHIDs(self::RELATION_REVIEWER);
}
public function getCCPHIDs() {
return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED);
}
private function getRelatedPHIDs($relation) {
$this->assertAttached($this->relationships);
return ipull($this->getRawRelations($relation), 'objectPHID');
}
public function getRawRelations($relation) {
return idx($this->relationships, $relation, array());
}
public function getPrimaryReviewer() {
$reviewers = $this->getReviewers();
$last = $this->lastReviewerPHID;
if (!$last || !in_array($last, $reviewers)) {
return head($this->getReviewers());
}
return $last;
}
public function getHashes() {
return $this->assertAttached($this->hashes);
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A revision's author (which effectively means "owner" after we added
// commandeering) can always view and edit it.
$author_phid = $this->getAuthorPHID();
if ($author_phid) {
if ($user->getPHID() == $author_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
$description = array(
pht('The owner of a revision can always view and edit it.'),
);
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$description[] = pht(
"A revision's reviewers can always view it.");
$description[] = pht(
'If a revision belongs to a repository, other users must be able '.
'to view the repository in order to view the revision.');
break;
}
return $description;
}
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
public function getReviewerStatus() {
return $this->assertAttached($this->reviewerStatus);
}
public function attachReviewerStatus(array $reviewers) {
assert_instances_of($reviewers, 'DifferentialReviewer');
$this->reviewerStatus = $reviewers;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function isClosed() {
return DifferentialRevisionStatus::isClosedStatus($this->getStatus());
}
public function getFlag(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
}
public function attachFlag(
PhabricatorUser $viewer,
PhabricatorFlag $flag = null) {
$this->flags[$viewer->getPHID()] = $flag;
return $this;
}
public function getDrafts(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getPHID());
}
public function attachDrafts(PhabricatorUser $viewer, array $drafts) {
$this->drafts[$viewer->getPHID()] = $drafts;
return $this;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildablePHID() {
return $this->loadActiveDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
if ($phid == $this->getAuthorPHID()) {
return true;
}
// TODO: This only happens when adding or removing CCs, and is safe from a
// policy perspective, but the subscription pathway should have some
// opportunity to load this data properly. For now, this is the only case
// where implicit subscription is not an intrinsic property of the object.
if ($this->reviewerStatus == self::ATTACHABLE) {
$reviewers = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needReviewerStatus(true)
->executeOne()
->getReviewerStatus();
} else {
$reviewers = $this->getReviewerStatus();
}
foreach ($reviewers as $reviewer) {
if ($reviewer->getReviewerPHID() == $phid) {
return true;
}
}
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('differential.fields');
}
public function getCustomFieldBaseClass() {
return 'DifferentialCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ $render_data = $timeline->getRenderData();
+ $left = $request->getInt('left', idx($render_data, 'left'));
+ $right = $request->getInt('right', idx($render_data, 'right'));
+
+ $diffs = id(new DifferentialDiffQuery())
+ ->setViewer($request->getUser())
+ ->withIDs(array($left, $right))
+ ->execute();
+ $diffs = mpull($diffs, null, 'getID');
+ $left_diff = $diffs[$left];
+ $right_diff = $diffs[$right];
+
+ $changesets = id(new DifferentialChangesetQuery())
+ ->setViewer($request->getUser())
+ ->withDiffs(array($right_diff))
+ ->execute();
+ $changesets = mpull($changesets, null, 'getID');
+
+ return $timeline
+ ->setChangesets($changesets)
+ ->setRevision($this)
+ ->setLeftDiff($left_diff)
+ ->setRightDiff($right_diff);
+ }
+
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$diffs = id(new DifferentialDiffQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($diffs as $diff) {
$engine->destroyObject($diff);
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
try {
$inlines = id(new DifferentialInlineCommentQuery())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($inlines as $inline) {
$inline->delete();
}
} catch (PhabricatorEmptyQueryException $ex) {
// TODO: There's still some funky legacy wrapping going on here, and
// we might catch a raw query exception.
}
// we have to do paths a little differentally as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php
index f2f45c1213..e7c0d63eb2 100644
--- a/src/applications/drydock/storage/DrydockBlueprint.php
+++ b/src/applications/drydock/storage/DrydockBlueprint.php
@@ -1,145 +1,152 @@
<?php
final class DrydockBlueprint extends DrydockDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $className;
protected $blueprintName;
protected $viewPolicy;
protected $editPolicy;
protected $details = array();
private $implementation = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
public static function initializeNewBlueprint(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDrydockApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DrydockDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
DrydockDefaultEditCapability::CAPABILITY);
return id(new DrydockBlueprint())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setBlueprintName('');
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'className' => 'text255',
'blueprintName' => 'text255',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DrydockBlueprintPHIDType::TYPECONST);
}
public function getImplementation() {
$class = $this->className;
$implementations =
DrydockBlueprintImplementation::getAllBlueprintImplementations();
if (!isset($implementations[$class])) {
throw new Exception(
"Invalid class name for blueprint (got '".$class."')");
}
return id(new $class())->attachInstance($this);
}
public function attachImplementation(DrydockBlueprintImplementation $impl) {
$this->implementation = $impl;
return $this;
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DrydockBlueprintEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DrydockBlueprintTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'DrydockBlueprintCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index fff4abddbd..1a4214a0c7 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1197 +1,1204 @@
<?php
/**
* Parameters
* ==========
*
* When creating a new file using a method like @{method:newFromFileData}, these
* parameters are supported:
*
* | name | Human readable filename.
* | authorPHID | User PHID of uploader.
* | ttl | Temporary file lifetime, in seconds.
* | viewPolicy | File visibility policy.
* | isExplicitUpload | Used to show users files they explicitly uploaded.
* | canCDN | Allows the file to be cached and delivered over a CDN.
* | mime-type | Optional, explicit file MIME type.
*
*/
final class PhabricatorFile extends PhabricatorFileDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const ONETIME_TEMPORARY_TOKEN_TYPE = 'file:onetime';
const STORAGE_FORMAT_RAW = 'raw';
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
const METADATA_CAN_CDN = 'canCDN';
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $metadata = array();
protected $mailKey;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
protected $ttl;
protected $isExplicitUpload = 1;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
private $objects = self::ATTACHABLE;
private $objectPHIDs = self::ATTACHABLE;
private $originalFile = self::ATTACHABLE;
public static function initializeNewFile() {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorFilesApplication'))
->executeOne();
$view_policy = $app->getPolicy(
FilesDefaultViewCapability::CAPABILITY);
return id(new PhabricatorFile())
->setViewPolicy($view_policy)
->attachOriginalFile(null)
->attachObjects(array())
->attachObjectPHIDs(array());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255?',
'mimeType' => 'text255?',
'byteSize' => 'uint64',
'storageEngine' => 'text32',
'storageFormat' => 'text32',
'storageHandle' => 'text255',
'authorPHID' => 'phid?',
'secretKey' => 'bytes20?',
'contentHash' => 'bytes40?',
'ttl' => 'epoch?',
'isExplicitUpload' => 'bool?',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'contentHash' => array(
'columns' => array('contentHash'),
),
'key_ttl' => array(
'columns' => array('ttl'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorFileFilePHIDType::TYPECONST);
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey($this->generateSecretKey());
}
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'F'.$this->getID();
}
public static function readUploadedFileData($spec) {
if (!$spec) {
throw new Exception('No file was uploaded!');
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
throw new Exception('File is not an uploaded file.');
}
$file_data = Filesystem::readFile($tmp_name);
$file_size = idx($spec, 'size');
if (strlen($file_data) != $file_size) {
throw new Exception('File size disagrees with uploaded size.');
}
self::validateFileSize(strlen($file_data));
return $file_data;
}
public static function newFromPHPUpload($spec, array $params = array()) {
$file_data = self::readUploadedFileData($spec);
$file_name = nonempty(
idx($params, 'name'),
idx($spec, 'name'));
$params = array(
'name' => $file_name,
) + $params;
return self::newFromFileData($file_data, $params);
}
public static function newFromXHRUpload($data, array $params = array()) {
self::validateFileSize(strlen($data));
return self::newFromFileData($data, $params);
}
private static function validateFileSize($size) {
$limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit');
if (!$limit) {
return;
}
$limit = phutil_parse_bytes($limit);
if ($size > $limit) {
throw new PhabricatorFileUploadException(-1000);
}
}
/**
* Given a block of data, try to load an existing file with the same content
* if one exists. If it does not, build a new file.
*
* This method is generally used when we have some piece of semi-trusted data
* like a diff or a file from a repository that we want to show to the user.
* We can't just dump it out because it may be dangerous for any number of
* reasons; instead, we need to serve it through the File abstraction so it
* ends up on the CDN domain if one is configured and so on. However, if we
* simply wrote a new file every time we'd potentially end up with a lot
* of redundant data in file storage.
*
* To solve these problems, we use file storage as a cache and reuse the
* same file again if we've previously written it.
*
* NOTE: This method unguards writes.
*
* @param string Raw file data.
* @param dict Dictionary of file information.
*/
public static function buildFromFileDataOrHash(
$data,
array $params = array()) {
$file = id(new PhabricatorFile())->loadOneWhere(
'name = %s AND contentHash = %s LIMIT 1',
self::normalizeFileName(idx($params, 'name')),
self::hashFileContent($data));
if (!$file) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData($data, $params);
unset($unguarded);
}
return $file;
}
public static function newFileFromContentHash($hash, array $params) {
// Check to see if a file with same contentHash exist
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1',
$hash);
if ($file) {
// copy storageEngine, storageHandle, storageFormat
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
$copy_of_byteSize = $file->getByteSize();
$copy_of_mimeType = $file->getMimeType();
$new_file = PhabricatorFile::initializeNewFile();
$new_file->setByteSize($copy_of_byteSize);
$new_file->setContentHash($hash);
$new_file->setStorageEngine($copy_of_storage_engine);
$new_file->setStorageHandle($copy_of_storage_handle);
$new_file->setStorageFormat($copy_of_storage_format);
$new_file->setMimeType($copy_of_mimeType);
$new_file->copyDimensions($file);
$new_file->readPropertiesFromParameters($params);
$new_file->save();
return $new_file;
}
return $file;
}
private static function buildFromFileData($data, array $params = array()) {
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$selector = PhabricatorEnv::newObjectFromConfig(
'storage.engine-selector');
$engines = $selector->selectStorageEngines($data, $params);
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception('No valid storage engines are available!');
}
$file = PhabricatorFile::initializeNewFile();
$data_handle = null;
$engine_identifier = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
list($engine_identifier, $data_handle) = $file->writeToEngine(
$engine,
$data,
$params);
// We stored the file somewhere so stop trying to write it to other
// places.
break;
} catch (PhabricatorFileStorageConfigurationException $ex) {
// If an engine is outright misconfigured (or misimplemented), raise
// that immediately since it probably needs attention.
throw $ex;
} catch (Exception $ex) {
phlog($ex);
// If an engine doesn't work, keep trying all the other valid engines
// in case something else works.
$exceptions[$engine_class] = $ex;
}
}
if (!$data_handle) {
throw new PhutilAggregateException(
'All storage engines failed to write file:',
$exceptions);
}
$file->setByteSize(strlen($data));
$file->setContentHash(self::hashFileContent($data));
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
// TODO: This is probably YAGNI, but allows for us to do encryption or
// compression later if we want.
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
$file->readPropertiesFromParameters($params);
if (!$file->getMimeType()) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
}
try {
$file->updateDimensions(false);
} catch (Exception $ex) {
// Do nothing
}
$file->save();
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
return self::buildFromFileData($data, $params);
}
public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
"You can not migrate a file which hasn't yet been saved.");
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
list($new_identifier, $new_handle) = $this->writeToEngine(
$engine,
$data,
$params);
$old_engine = $this->instantiateStorageEngine();
$old_identifier = $this->getStorageEngine();
$old_handle = $this->getStorageHandle();
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->save();
$this->deleteFileDataIfUnused(
$old_engine,
$old_identifier,
$old_handle);
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$data_handle = $engine->writeFile($data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
"Storage engine '{$engine_class}' executed writeFile() but did ".
"not return a valid handle ('{$data_handle}') to the data: it ".
"must be nonempty and no longer than 255 characters.");
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
"Storage engine '{$engine_class}' returned an improper engine ".
"identifier '{$engine_identifier}': it must be nonempty ".
"and no longer than 32 characters.");
}
return array($engine_identifier, $data_handle);
}
public static function newFromFileDownload($uri, array $params = array()) {
// Make sure we're allowed to make a request first
if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) {
throw new Exception('Outbound HTTP requests are disabled!');
}
$uri = new PhutilURI($uri);
$protocol = $uri->getProtocol();
switch ($protocol) {
case 'http':
case 'https':
break;
default:
// Make sure we are not accessing any file:// URIs or similar.
return null;
}
$timeout = 5;
list($file_data) = id(new HTTPSFuture($uri))
->setTimeout($timeout)
->resolvex();
$params = $params + array(
'name' => basename($uri),
);
return self::newFromFileData($file_data, $params);
}
public static function normalizeFileName($file_name) {
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
$file_name = preg_replace($pattern, '_', $file_name);
$file_name = preg_replace('@_+@', '_', $file_name);
$file_name = trim($file_name, '_');
$disallowed_filenames = array(
'.' => 'dot',
'..' => 'dotdot',
'' => 'file',
);
$file_name = idx($disallowed_filenames, $file_name, $file_name);
return $file_name;
}
public function delete() {
// We want to delete all the rows which mark this file as the transformation
// of some other file (since we're getting rid of it). We also delete all
// the transformations of this file, so that a user who deletes an image
// doesn't need to separately hunt down and delete a bunch of thumbnails and
// resizes of it.
$outbound_xforms = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms(
array(
array(
'originalPHID' => $this->getPHID(),
'transform' => true,
),
))
->execute();
foreach ($outbound_xforms as $outbound_xform) {
$outbound_xform->delete();
}
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID = %s',
$this->getPHID());
$this->openTransaction();
foreach ($inbound_xforms as $inbound_xform) {
$inbound_xform->delete();
}
$ret = parent::delete();
$this->saveTransaction();
$this->deleteFileDataIfUnused(
$this->instantiateStorageEngine(),
$this->getStorageEngine(),
$this->getStorageHandle());
return $ret;
}
/**
* Destroy stored file data if there are no remaining files which reference
* it.
*/
public function deleteFileDataIfUnused(
PhabricatorFileStorageEngine $engine,
$engine_identifier,
$handle) {
// Check to see if any files are using storage.
$usage = id(new PhabricatorFile())->loadAllWhere(
'storageEngine = %s AND storageHandle = %s LIMIT 1',
$engine_identifier,
$handle);
// If there are no files using the storage, destroy the actual storage.
if (!$usage) {
try {
$engine->deleteFile($handle);
} catch (Exception $ex) {
// In the worst case, we're leaving some data stranded in a storage
// engine, which is not a big deal.
phlog($ex);
}
}
}
public static function hashFileContent($data) {
return sha1($data);
}
public function loadFileData() {
$engine = $this->instantiateStorageEngine();
$data = $engine->readFile($this->getStorageHandle());
switch ($this->getStorageFormat()) {
case self::STORAGE_FORMAT_RAW:
$data = $data;
break;
default:
throw new Exception('Unknown storage format.');
}
return $data;
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
'You must save a file before you can generate a view URI.');
}
$name = phutil_escape_uri($this->getName());
$path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
return PhabricatorEnv::getCDNURI($path);
}
public function getInfoURI() {
return '/'.$this->getMonogram();
}
public function getBestURI() {
if ($this->isViewableInBrowser()) {
return $this->getViewURI();
} else {
return $this->getInfoURI();
}
}
public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI()))
->setQueryParam('download', true);
return (string) $uri;
}
public function getProfileThumbURI() {
$path = '/file/xform/thumb-profile/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb60x45URI() {
$path = '/file/xform/thumb-60x45/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb160x120URI() {
$path = '/file/xform/thumb-160x120/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getPreview100URI() {
$path = '/file/xform/preview-100/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getPreview220URI() {
$path = '/file/xform/preview-220/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb220x165URI() {
$path = '/file/xform/thumb-220x165/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb280x210URI() {
$path = '/file/xform/thumb-280x210/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}
public function isViewableImage() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isAudio() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
// warns you if you don't have complete support.
$matches = null;
$ok = preg_match(
'@^image/(gif|png|jpe?g)@',
$this->getViewableMimeType(),
$matches);
if (!$ok) {
return false;
}
switch ($matches[1]) {
case 'jpg';
case 'jpeg':
return function_exists('imagejpeg');
break;
case 'png':
return function_exists('imagepng');
break;
case 'gif':
return function_exists('imagegif');
break;
default:
throw new Exception('Unknown type matched as image MIME type.');
}
}
public static function getTransformableImageFormats() {
$supported = array();
if (function_exists('imagejpeg')) {
$supported[] = 'jpg';
}
if (function_exists('imagepng')) {
$supported[] = 'png';
}
if (function_exists('imagegif')) {
$supported[] = 'gif';
}
return $supported;
}
public function instantiateStorageEngine() {
return self::buildEngine($this->getStorageEngine());
}
public static function buildEngine($engine_identifier) {
$engines = self::buildAllEngines();
foreach ($engines as $engine) {
if ($engine->getEngineIdentifier() == $engine_identifier) {
return $engine;
}
}
throw new Exception(
"Storage engine '{$engine_identifier}' could not be located!");
}
public static function buildAllEngines() {
$engines = id(new PhutilSymbolLoader())
->setType('class')
->setConcreteOnly(true)
->setAncestorClass('PhabricatorFileStorageEngine')
->selectAndLoadSymbols();
$results = array();
foreach ($engines as $engine_class) {
$results[] = newv($engine_class['name'], array());
}
return $results;
}
public function getViewableMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
$mime_type = $this->getMimeType();
$mime_parts = explode(';', $mime_type);
$mime_type = trim(reset($mime_parts));
return idx($mime_map, $mime_type);
}
public function getDisplayIconForMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type, 'docs_file');
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
public function updateDimensions($save = true) {
if (!$this->isViewableImage()) {
throw new Exception(
'This file is not a viewable image.');
}
if (!function_exists('imagecreatefromstring')) {
throw new Exception(
'Cannot retrieve image information.');
}
$data = $this->loadFileData();
$img = imagecreatefromstring($data);
if ($img === false) {
throw new Exception(
'Error when decoding image.');
}
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
if ($save) {
$this->save();
}
return $this;
}
public function copyDimensions(PhabricatorFile $file) {
$metadata = $file->getMetadata();
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
if ($width) {
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
}
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
if ($height) {
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
}
return $this;
}
/**
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
* resources. The builtin mechanism allows files shipped with Phabricator
* to be treated like normal files so that APIs do not need to special case
* things like default images or deleted files.
*
* Builtins are located in `resources/builtin/` and identified by their
* name.
*
* @param PhabricatorUser Viewing user.
* @param list<string> List of builtin file names.
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
*/
public static function loadBuiltins(PhabricatorUser $user, array $names) {
$specs = array();
foreach ($names as $name) {
$specs[] = array(
'originalPHID' => PhabricatorPHIDConstants::PHID_VOID,
'transform' => 'builtin:'.$name,
);
}
// NOTE: Anyone is allowed to access builtin files.
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms($specs)
->execute();
$files = mpull($files, null, 'getName');
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/';
$build = array();
foreach ($names as $name) {
if (isset($files[$name])) {
continue;
}
// This is just a sanity check to prevent loading arbitrary files.
if (basename($name) != $name) {
throw new Exception("Invalid builtin name '{$name}'!");
}
$path = $root.$name;
if (!Filesystem::pathExists($path)) {
throw new Exception("Builtin '{$path}' does not exist!");
}
$data = Filesystem::readFile($path);
$params = array(
'name' => $name,
'ttl' => time() + (60 * 60 * 24 * 7),
);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData($data, $params);
$xform = id(new PhabricatorTransformedFile())
->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID)
->setTransform('builtin:'.$name)
->setTransformedPHID($file->getPHID())
->save();
unset($unguarded);
$file->attachObjectPHIDs(array());
$file->attachObjects(array());
$files[$name] = $file;
}
return $files;
}
/**
* Convenience wrapper for @{method:loadBuiltins}.
*
* @param PhabricatorUser Viewing user.
* @param string Single builtin name to load.
* @return PhabricatorFile Corresponding builtin file.
*/
public static function loadBuiltin(PhabricatorUser $user, $name) {
return idx(self::loadBuiltins($user, array($name)), $name);
}
public function getObjects() {
return $this->assertAttached($this->objects);
}
public function attachObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObjectPHIDs() {
return $this->assertAttached($this->objectPHIDs);
}
public function attachObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function getOriginalFile() {
return $this->assertAttached($this->originalFile);
}
public function attachOriginalFile(PhabricatorFile $file = null) {
$this->originalFile = $file;
return $this;
}
public function getImageHeight() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
}
public function getImageWidth() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
}
public function getCanCDN() {
if (!$this->isViewableImage()) {
return false;
}
return idx($this->metadata, self::METADATA_CAN_CDN);
}
public function setCanCDN($can_cdn) {
$this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
return $this;
}
protected function generateOneTimeToken() {
$key = Filesystem::readRandomCharacters(16);
// Save the new secret.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token = id(new PhabricatorAuthTemporaryToken())
->setObjectPHID($this->getPHID())
->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->save();
unset($unguarded);
return $key;
}
public function validateOneTimeToken($token_code) {
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($this->getPHID()))
->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
->withExpired(false)
->withTokenCodes(array(PhabricatorHash::digest($token_code)))
->executeOne();
return $token;
}
/** Get the CDN uri for this file
* This will generate a one-time-use token if
* security.alternate_file_domain is set in the config.
*/
public function getCDNURIWithToken() {
if (!$this->getPHID()) {
throw new Exception(
'You must save a file before you can generate a CDN URI.');
}
$name = phutil_escape_uri($this->getName());
$path = '/file/data'
.'/'.$this->getSecretKey()
.'/'.$this->getPHID()
.'/'.$this->generateOneTimeToken()
.'/'.$name;
return PhabricatorEnv::getCDNURI($path);
}
/**
* Write the policy edge between this file and some object.
*
* @param phid Object PHID to attach to.
* @return this
*/
public function attachToObject($phid) {
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
id(new PhabricatorEdgeEditor())
->addEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Remove the policy edge between this file and some object.
*
* @param phid Object PHID to detach from.
* @return this
*/
public function detachFromObject($phid) {
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
id(new PhabricatorEdgeEditor())
->removeEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Configure a newly created file object according to specified parameters.
*
* This method is called both when creating a file from fresh data, and
* when creating a new file which reuses existing storage.
*
* @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile}
* for documentation.
* @return this
*/
private function readPropertiesFromParameters(array $params) {
$file_name = idx($params, 'name');
$file_name = self::normalizeFileName($file_name);
$this->setName($file_name);
$author_phid = idx($params, 'authorPHID');
$this->setAuthorPHID($author_phid);
$file_ttl = idx($params, 'ttl');
$this->setTtl($file_ttl);
$view_policy = idx($params, 'viewPolicy');
if ($view_policy) {
$this->setViewPolicy($params['viewPolicy']);
}
$is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
$this->setIsExplicitUpload($is_explicit);
$can_cdn = idx($params, 'canCDN');
if ($can_cdn) {
$this->setCanCDN(true);
}
$mime_type = idx($params, 'mime-type');
if ($mime_type) {
$this->setMimeType($mime_type);
}
return $this;
}
public function getRedirectResponse() {
$uri = $this->getBestURI();
// TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
// (if the file is a viewable image) and sometimes a local URI (if not).
// For now, just detect which one we got and configure the response
// appropriately. In the long run, if this endpoint is served from a CDN
// domain, we can't issue a local redirect to an info URI (which is not
// present on the CDN domain). We probably never actually issue local
// redirects here anyway, since we only ever transform viewable images
// right now.
$is_external = strlen(id(new PhutilURI($uri))->getDomain());
return id(new AphrontRedirectResponse())
->setIsExternal($is_external)
->setURI($uri);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorFileEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorFileTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
if ($this->getAuthorPHID() == $viewer_phid) {
return true;
}
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If you can see the file this file is a transform of, you can see
// this file.
if ($this->getOriginalFile()) {
return true;
}
// If you can see any object this file is attached to, you can see
// the file.
return (count($this->getObjects()) > 0);
}
return false;
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who uploaded a file can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'Files attached to objects are visible to users who can view '.
'those objects.');
$out[] = pht(
'Thumbnails are visible only to users who can view the original '.
'file.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php
index dd66f8cd6e..d9543bb225 100644
--- a/src/applications/fund/storage/FundBacker.php
+++ b/src/applications/fund/storage/FundBacker.php
@@ -1,121 +1,128 @@
<?php
final class FundBacker extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $initiativePHID;
protected $backerPHID;
protected $amountAsCurrency;
protected $status;
protected $properties = array();
private $initiative = self::ATTACHABLE;
const STATUS_NEW = 'new';
const STATUS_IN_CART = 'in-cart';
const STATUS_PURCHASED = 'purchased';
public static function initializeNewBacker(PhabricatorUser $actor) {
return id(new FundBacker())
->setBackerPHID($actor->getPHID())
->setStatus(self::STATUS_NEW);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'amountAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'amountAsCurrency' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_initiative' => array(
'columns' => array('initiativePHID'),
),
'key_backer' => array(
'columns' => array('backerPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getInitiative() {
return $this->assertAttached($this->initiative);
}
public function attachInitiative(FundInitiative $initiative = null) {
$this->initiative = $initiative;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If we have the initiative, use the initiative's policy.
// Otherwise, return NOONE. This allows the backer to continue seeing
// a backer even if they're no longer allowed to see the initiative.
$initiative = $this->getInitiative();
if ($initiative) {
return $initiative->getPolicy($capability);
}
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getBackerPHID());
}
public function describeAutomaticCapability($capability) {
return pht('A backer can always see what they have backed.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundBackerEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundBackerTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
}
diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php
index 8893c05619..1a266e3047 100644
--- a/src/applications/fund/storage/FundInitiative.php
+++ b/src/applications/fund/storage/FundInitiative.php
@@ -1,190 +1,197 @@
<?php
final class FundInitiative extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $ownerPHID;
protected $merchantPHID;
protected $description;
protected $risks;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $totalAsCurrency;
protected $mailKey;
private $projectPHIDs = self::ATTACHABLE;
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
public static function getStatusNameMap() {
return array(
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
);
}
public static function initializeNewInitiative(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorFundApplication'))
->executeOne();
$view_policy = $app->getPolicy(FundDefaultViewCapability::CAPABILITY);
return id(new FundInitiative())
->setOwnerPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($actor->getPHID())
->setStatus(self::STATUS_OPEN)
->setTotalAsCurrency(PhortuneCurrency::newEmptyCurrency());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'description' => 'text',
'risks' => 'text',
'status' => 'text32',
'merchantPHID' => 'phid?',
'totalAsCurrency' => 'text64',
'mailKey' => 'bytes20',
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'totalAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('status'),
),
'key_owner' => array(
'columns' => array('ownerPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundInitiativePHIDType::TYPECONST);
}
public function getMonogram() {
return 'I'.$this->getID();
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function isClosed() {
return ($this->getStatus() == self::STATUS_CLOSED);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getOwnerPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'The owner of an initiative can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundInitiativeEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundInitiativeTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenRecevierInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getOwnerPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php
index 17cfd8350f..3026837628 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildable.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php
@@ -1,295 +1,302 @@
<?php
final class HarbormasterBuildable extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
HarbormasterBuildableInterface {
protected $buildablePHID;
protected $containerPHID;
protected $buildableStatus;
protected $isManualBuildable;
private $buildableObject = self::ATTACHABLE;
private $containerObject = self::ATTACHABLE;
private $buildableHandle = self::ATTACHABLE;
private $containerHandle = self::ATTACHABLE;
private $builds = self::ATTACHABLE;
const STATUS_BUILDING = 'building';
const STATUS_PASSED = 'passed';
const STATUS_FAILED = 'failed';
public static function getBuildableStatusName($status) {
switch ($status) {
case self::STATUS_BUILDING:
return pht('Building');
case self::STATUS_PASSED:
return pht('Passed');
case self::STATUS_FAILED:
return pht('Failed');
default:
return pht('Unknown');
}
}
public static function getBuildableStatusIcon($status) {
switch ($status) {
case self::STATUS_BUILDING:
return PHUIStatusItemView::ICON_RIGHT;
case self::STATUS_PASSED:
return PHUIStatusItemView::ICON_ACCEPT;
case self::STATUS_FAILED:
return PHUIStatusItemView::ICON_REJECT;
default:
return PHUIStatusItemView::ICON_QUESTION;
}
}
public static function getBuildableStatusColor($status) {
switch ($status) {
case self::STATUS_BUILDING:
return 'blue';
case self::STATUS_PASSED:
return 'green';
case self::STATUS_FAILED:
return 'red';
default:
return 'bluegrey';
}
}
public static function initializeNewBuildable(PhabricatorUser $actor) {
return id(new HarbormasterBuildable())
->setIsManualBuildable(0)
->setBuildableStatus(self::STATUS_BUILDING);
}
public function getMonogram() {
return 'B'.$this->getID();
}
/**
* Returns an existing buildable for the object's PHID or creates a
* new buildable implicitly if needed.
*/
public static function createOrLoadExisting(
PhabricatorUser $actor,
$buildable_object_phid,
$container_object_phid) {
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($actor)
->withBuildablePHIDs(array($buildable_object_phid))
->withManualBuildables(false)
->setLimit(1)
->executeOne();
if ($buildable) {
return $buildable;
}
$buildable = HarbormasterBuildable::initializeNewBuildable($actor)
->setBuildablePHID($buildable_object_phid)
->setContainerPHID($container_object_phid);
$buildable->save();
return $buildable;
}
/**
* Looks up the plan PHIDs and applies the plans to the specified
* object identified by it's PHID.
*/
public static function applyBuildPlans(
$phid,
$container_phid,
array $plan_phids) {
if (count($plan_phids) === 0) {
return;
}
// Skip all of this logic if the Harbormaster application
// isn't currently installed.
$harbormaster_app = 'PhabricatorHarbormasterApplication';
if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) {
return;
}
$buildable = HarbormasterBuildable::createOrLoadExisting(
PhabricatorUser::getOmnipotentUser(),
$phid,
$container_phid);
$plans = id(new HarbormasterBuildPlanQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($plan_phids)
->execute();
foreach ($plans as $plan) {
if ($plan->isDisabled()) {
// TODO: This should be communicated more clearly -- maybe we should
// create the build but set the status to "disabled" or "derelict".
continue;
}
$buildable->applyPlan($plan);
}
}
public function applyPlan(HarbormasterBuildPlan $plan) {
$viewer = PhabricatorUser::getOmnipotentUser();
$build = HarbormasterBuild::initializeNewBuild($viewer)
->setBuildablePHID($this->getPHID())
->setBuildPlanPHID($plan->getPHID())
->setBuildStatus(HarbormasterBuild::STATUS_PENDING)
->save();
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildID' => $build->getID(),
));
return $build;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'containerPHID' => 'phid?',
'buildableStatus' => 'text32',
'isManualBuildable' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_container' => array(
'columns' => array('containerPHID'),
),
'key_manual' => array(
'columns' => array('isManualBuildable'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildablePHIDType::TYPECONST);
}
public function attachBuildableObject($buildable_object) {
$this->buildableObject = $buildable_object;
return $this;
}
public function getBuildableObject() {
return $this->assertAttached($this->buildableObject);
}
public function attachContainerObject($container_object) {
$this->containerObject = $container_object;
return $this;
}
public function getContainerObject() {
return $this->assertAttached($this->containerObject);
}
public function attachContainerHandle($container_handle) {
$this->containerHandle = $container_handle;
return $this;
}
public function getContainerHandle() {
return $this->assertAttached($this->containerHandle);
}
public function attachBuildableHandle($buildable_handle) {
$this->buildableHandle = $buildable_handle;
return $this;
}
public function getBuildableHandle() {
return $this->assertAttached($this->buildableHandle);
}
public function attachBuilds(array $builds) {
assert_instances_of($builds, 'HarbormasterBuild');
$this->builds = $builds;
return $this;
}
public function getBuilds() {
return $this->assertAttached($this->builds);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildableTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildableTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildableObject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildableObject()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A buildable inherits policies from the underlying object.');
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildablePHID() {
// NOTE: This is essentially just for convenience, as it allows you create
// a copy of a buildable by specifying `B123` without bothering to go
// look up the underlying object.
return $this->getBuildablePHID();
}
public function getHarbormasterContainerPHID() {
return $this->getContainerPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
index db8cbd2818..3f4295e45e 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,447 +1,454 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $buildGeneration;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
private $buildTargets = self::ATTACHABLE;
private $unprocessedCommands = self::ATTACHABLE;
/**
* Not currently being built.
*/
const STATUS_INACTIVE = 'inactive';
/**
* Pending pick up by the Harbormaster daemon.
*/
const STATUS_PENDING = 'pending';
/**
* Current building the buildable.
*/
const STATUS_BUILDING = 'building';
/**
* The build has passed.
*/
const STATUS_PASSED = 'passed';
/**
* The build has failed.
*/
const STATUS_FAILED = 'failed';
/**
* The build encountered an unexpected error.
*/
const STATUS_ERROR = 'error';
/**
* The build has been stopped.
*/
const STATUS_STOPPED = 'stopped';
/**
* The build has been deadlocked.
*/
const STATUS_DEADLOCKED = 'deadlocked';
/**
* Get a human readable name for a build status constant.
*
* @param const Build status constant.
* @return string Human-readable name.
*/
public static function getBuildStatusName($status) {
switch ($status) {
case self::STATUS_INACTIVE:
return pht('Inactive');
case self::STATUS_PENDING:
return pht('Pending');
case self::STATUS_BUILDING:
return pht('Building');
case self::STATUS_PASSED:
return pht('Passed');
case self::STATUS_FAILED:
return pht('Failed');
case self::STATUS_ERROR:
return pht('Unexpected Error');
case self::STATUS_STOPPED:
return pht('Paused');
case self::STATUS_DEADLOCKED:
return pht('Deadlocked');
default:
return pht('Unknown');
}
}
public static function getBuildStatusIcon($status) {
switch ($status) {
case self::STATUS_INACTIVE:
case self::STATUS_PENDING:
return PHUIStatusItemView::ICON_OPEN;
case self::STATUS_BUILDING:
return PHUIStatusItemView::ICON_RIGHT;
case self::STATUS_PASSED:
return PHUIStatusItemView::ICON_ACCEPT;
case self::STATUS_FAILED:
return PHUIStatusItemView::ICON_REJECT;
case self::STATUS_ERROR:
return PHUIStatusItemView::ICON_MINUS;
case self::STATUS_STOPPED:
return PHUIStatusItemView::ICON_MINUS;
case self::STATUS_DEADLOCKED:
return PHUIStatusItemView::ICON_WARNING;
default:
return PHUIStatusItemView::ICON_QUESTION;
}
}
public static function getBuildStatusColor($status) {
switch ($status) {
case self::STATUS_INACTIVE:
return 'dark';
case self::STATUS_PENDING:
case self::STATUS_BUILDING:
return 'blue';
case self::STATUS_PASSED:
return 'green';
case self::STATUS_FAILED:
case self::STATUS_ERROR:
case self::STATUS_DEADLOCKED:
return 'red';
case self::STATUS_STOPPED:
return 'dark';
default:
return 'bluegrey';
}
}
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(self::STATUS_INACTIVE)
->setBuildGeneration(0);
}
public function delete() {
$this->openTransaction();
$this->deleteUnprocessedCommands();
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'buildStatus' => 'text32',
'buildGeneration' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_status' => array(
'columns' => array('buildStatus'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPHIDType::TYPECONST);
}
public function attachBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getName() {
if ($this->getBuildPlan()) {
return $this->getBuildPlan()->getName();
}
return pht('Build');
}
public function attachBuildPlan(
HarbormasterBuildPlan $build_plan = null) {
$this->buildPlan = $build_plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getBuildTargets() {
return $this->assertAttached($this->buildTargets);
}
public function attachBuildTargets(array $targets) {
$this->buildTargets = $targets;
return $this;
}
public function isBuilding() {
return $this->getBuildStatus() === self::STATUS_PENDING ||
$this->getBuildStatus() === self::STATUS_BUILDING;
}
public function createLog(
HarbormasterBuildTarget $build_target,
$log_source,
$log_type) {
$log_source = id(new PhutilUTF8StringTruncator())
->setMaximumCodepoints(250)
->truncateString($log_source);
$log = HarbormasterBuildLog::initializeNewBuildLog($build_target)
->setLogSource($log_source)
->setLogType($log_type)
->save();
return $log;
}
public function createArtifact(
HarbormasterBuildTarget $build_target,
$artifact_key,
$artifact_type) {
$artifact =
HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target);
$artifact->setArtifactKey(
$this->getPHID(),
$this->getBuildGeneration(),
$artifact_key);
$artifact->setArtifactType($artifact_type);
$artifact->save();
return $artifact;
}
public function loadArtifact($name) {
$artifact = id(new HarbormasterBuildArtifactQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withArtifactKeys(
$this->getPHID(),
$this->getBuildGeneration(),
array($name))
->executeOne();
if ($artifact === null) {
throw new Exception('Artifact not found!');
}
return $artifact;
}
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
'buildable.revision' => null,
'buildable.commit' => null,
'repository.callsign' => null,
'repository.vcs' => null,
'repository.uri' => null,
'step.timestamp' => null,
'build.id' => null,
);
$buildable = $this->getBuildable();
$object = $buildable->getBuildableObject();
$object_variables = $object->getBuildVariables();
$results = $object_variables + $results;
$results['step.timestamp'] = time();
$results['build.id'] = $this->getID();
return $results;
}
public static function getAvailableBuildVariables() {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass('HarbormasterBuildableInterface')
->loadObjects();
$variables = array();
$variables[] = array(
'step.timestamp' => pht('The current UNIX timestamp.'),
'build.id' => pht('The ID of the current build.'),
'target.phid' => pht('The PHID of the current build target.'),
);
foreach ($objects as $object) {
$variables[] = $object->getAvailableBuildVariables();
}
$variables = array_mergev($variables);
return $variables;
}
public function isComplete() {
switch ($this->getBuildStatus()) {
case self::STATUS_PASSED:
case self::STATUS_FAILED:
case self::STATUS_ERROR:
case self::STATUS_STOPPED:
return true;
}
return false;
}
public function isStopped() {
return ($this->getBuildStatus() == self::STATUS_STOPPED);
}
/* -( Build Commands )----------------------------------------------------- */
private function getUnprocessedCommands() {
return $this->assertAttached($this->unprocessedCommands);
}
public function attachUnprocessedCommands(array $commands) {
$this->unprocessedCommands = $commands;
return $this;
}
public function canRestartBuild() {
return !$this->isRestarting();
}
public function canStopBuild() {
return !$this->isComplete() &&
!$this->isStopped() &&
!$this->isStopping();
}
public function canResumeBuild() {
return $this->isStopped() &&
!$this->isResuming();
}
public function isStopping() {
$is_stopping = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_STOP:
$is_stopping = true;
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_stopping = false;
break;
}
}
return $is_stopping;
}
public function isResuming() {
$is_resuming = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
break;
case HarbormasterBuildCommand::COMMAND_STOP:
$is_resuming = false;
break;
}
}
return $is_resuming;
}
public function isRestarting() {
$is_restarting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
break;
}
}
return $is_restarting;
}
public function deleteUnprocessedCommands() {
foreach ($this->getUnprocessedCommands() as $key => $command_object) {
$command_object->delete();
unset($this->unprocessedCommands[$key]);
}
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildable()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildable()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build inherits policies from its buildable.');
}
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
index 3475f00a4f..594ad3c9d4 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
@@ -1,112 +1,119 @@
<?php
final class HarbormasterBuildPlan extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface {
protected $name;
protected $planStatus;
const STATUS_ACTIVE = 'active';
const STATUS_DISABLED = 'disabled';
private $buildSteps = self::ATTACHABLE;
public static function initializeNewBuildPlan(PhabricatorUser $actor) {
return id(new HarbormasterBuildPlan())
->setPlanStatus(self::STATUS_ACTIVE);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'planStatus' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('planStatus'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPlanPHIDType::TYPECONST);
}
public function attachBuildSteps(array $steps) {
assert_instances_of($steps, 'HarbormasterBuildStep');
$this->buildSteps = $steps;
return $this;
}
public function getBuildSteps() {
return $this->assertAttached($this->buildSteps);
}
public function isDisabled() {
return ($this->getPlanStatus() == self::STATUS_DISABLED);
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildPlanEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildPlanTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
index 858b57abc8..92b8e69ccf 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
@@ -1,151 +1,158 @@
<?php
final class HarbormasterBuildStep extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $name;
protected $description;
protected $buildPlanPHID;
protected $className;
protected $details = array();
protected $sequence = 0;
private $buildPlan = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $implementation;
public static function initializeNewStep(PhabricatorUser $actor) {
return id(new HarbormasterBuildStep());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'className' => 'text255',
'sequence' => 'uint32',
'description' => 'text',
// T6203/NULLABILITY
// This should not be nullable. Current `null` values indicate steps
// which predated editable names. These should be backfilled with
// default names, then the code for handling `null` shoudl be removed.
'name' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildStepPHIDType::TYPECONST);
}
public function attachBuildPlan(HarbormasterBuildPlan $plan) {
$this->buildPlan = $plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getName() {
if (strlen($this->name)) {
return $this->name;
}
return $this->getStepImplementation()->getName();
}
public function getStepImplementation() {
if ($this->implementation === null) {
$obj = HarbormasterBuildStepImplementation::requireImplementation(
$this->className);
$obj->loadSettings($this);
$this->implementation = $obj;
}
return $this->implementation;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildStepEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildStepTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getBuildPlan()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildPlan()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build step has the same policies as its build plan.');
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'HarbormasterBuildStepCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index 95908714cb..6289ac8797 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,339 +1,349 @@
<?php
final class HeraldRule extends HeraldDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';
protected $name;
protected $authorPHID;
protected $contentType;
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $isDisabled = 0;
protected $triggerObjectPHID;
protected $configVersion = 38;
// PHIDs for which this rule has been applied
private $ruleApplied = self::ATTACHABLE;
private $validAuthor = self::ATTACHABLE;
private $author = self::ATTACHABLE;
private $conditions;
private $actions;
private $triggerObject = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'contentType' => 'text255',
'mustMatchAll' => 'bool',
'configVersion' => 'uint32',
'ruleType' => 'text32',
'isDisabled' => 'uint32',
'triggerObjectPHID' => 'phid?',
// T6203/NULLABILITY
// This should not be nullable.
'repetitionPolicy' => 'uint32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_ruletype' => array(
'columns' => array('ruleType'),
),
'key_trigger' => array(
'columns' => array('triggerObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST);
}
public function getRuleApplied($phid) {
return $this->assertAttachedKey($this->ruleApplied, $phid);
}
public function setRuleApplied($phid, $applied) {
if ($this->ruleApplied === self::ATTACHABLE) {
$this->ruleApplied = array();
}
$this->ruleApplied[$phid] = $applied;
return $this;
}
public function loadConditions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldCondition())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
$this->conditions = $conditions;
return $this;
}
public function getConditions() {
// TODO: validate conditions have been attached.
return $this->conditions;
}
public function loadActions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldAction())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachActions(array $actions) {
// TODO: validate actions have been attached.
assert_instances_of($actions, 'HeraldAction');
$this->actions = $actions;
return $this;
}
public function getActions() {
return $this->actions;
}
public function loadEdits() {
if (!$this->getID()) {
return array();
}
$edits = id(new HeraldRuleEdit())->loadAllWhere(
'ruleID = %d ORDER BY dateCreated DESC',
$this->getID());
return $edits;
}
public function logEdit($editor_phid, $action) {
id(new HeraldRuleEdit())
->setRuleID($this->getID())
->setRuleName($this->getName())
->setEditorPHID($editor_phid)
->setAction($action)
->save();
}
public function saveConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
return $this->saveChildren(
id(new HeraldCondition())->getTableName(),
$conditions);
}
public function saveActions(array $actions) {
assert_instances_of($actions, 'HeraldAction');
return $this->saveChildren(
id(new HeraldAction())->getTableName(),
$actions);
}
protected function saveChildren($table_name, array $children) {
assert_instances_of($children, 'HeraldDAO');
if (!$this->getID()) {
throw new Exception('Save rule before saving children.');
}
foreach ($children as $child) {
$child->setRuleID($this->getID());
}
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
$table_name,
$this->getID());
foreach ($children as $child) {
$child->save();
}
$this->saveTransaction();
}
public function delete() {
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldCondition())->getTableName(),
$this->getID());
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldAction())->getTableName(),
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function hasValidAuthor() {
return $this->assertAttached($this->validAuthor);
}
public function attachValidAuthor($valid) {
$this->validAuthor = $valid;
return $this;
}
public function getAuthor() {
return $this->assertAttached($this->author);
}
public function attachAuthor(PhabricatorUser $user) {
$this->author = $user;
return $this;
}
public function isGlobalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL);
}
public function isPersonalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function isObjectRule() {
return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
}
public function attachTriggerObject($trigger_object) {
$this->triggerObject = $trigger_object;
return $this;
}
public function getTriggerObject() {
return $this->assertAttached($this->triggerObject);
}
/**
* Get a sortable key for rule execution order.
*
* Rules execute in a well-defined order: personal rules first, then object
* rules, then global rules. Within each rule type, rules execute from lowest
* ID to highest ID.
*
* This ordering allows more powerful rules (like global rules) to override
* weaker rules (like personal rules) when multiple rules exist which try to
* affect the same field. Executing from low IDs to high IDs makes
* interactions easier to understand when adding new rules, because the newest
* rules always happen last.
*
* @return string A sortable key for this rule.
*/
public function getRuleExecutionOrderSortKey() {
$rule_type = $this->getRuleType();
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$type_order = 1;
break;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$type_order = 2;
break;
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$type_order = 3;
break;
default:
throw new Exception(pht('Unknown rule type "%s"!', $rule_type));
}
return sprintf('~%d%010d', $type_order, $this->getID());
}
+
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
public function getApplicationTransactionEditor() {
return new HeraldRuleEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HeraldRuleTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($this->isGlobalRule()) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
$app = 'PhabricatorHeraldApplication';
$herald = PhabricatorApplication::getByClass($app);
$global = HeraldManageGlobalRulesCapability::CAPABILITY;
return $herald->getPolicy($global);
}
} else if ($this->isObjectRule()) {
return $this->getTriggerObject()->getPolicy($capability);
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isPersonalRule()) {
return ($viewer->getPHID() == $this->getAuthorPHID());
} else {
return false;
}
}
public function describeAutomaticCapability($capability) {
if ($this->isPersonalRule()) {
return pht("A personal rule's owner can always view and edit it.");
} else if ($this->isObjectRule()) {
return pht('Object rules inherit the policies of their objects.');
}
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php
index ca3b035cb0..e518e3be5f 100644
--- a/src/applications/legalpad/storage/LegalpadDocument.php
+++ b/src/applications/legalpad/storage/LegalpadDocument.php
@@ -1,243 +1,250 @@
<?php
final class LegalpadDocument extends LegalpadDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $title;
protected $contributorCount;
protected $recentContributorPHIDs = array();
protected $creatorPHID;
protected $versions;
protected $documentBodyPHID;
protected $viewPolicy;
protected $editPolicy;
protected $mailKey;
protected $signatureType;
protected $preamble;
const SIGNATURE_TYPE_INDIVIDUAL = 'user';
const SIGNATURE_TYPE_CORPORATION = 'corp';
private $documentBody = self::ATTACHABLE;
private $contributors = self::ATTACHABLE;
private $signatures = self::ATTACHABLE;
private $userSignatures = array();
public static function initializeNewDocument(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorLegalpadApplication'))
->executeOne();
$view_policy = $app->getPolicy(LegalpadDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(LegalpadDefaultEditCapability::CAPABILITY);
return id(new LegalpadDocument())
->setVersions(0)
->setCreatorPHID($actor->getPHID())
->setContributorCount(0)
->setRecentContributorPHIDs(array())
->attachSignatures(array())
->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL)
->setPreamble('')
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'recentContributorPHIDs' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'contributorCount' => 'uint32',
'versions' => 'uint32',
'mailKey' => 'bytes20',
'signatureType' => 'text4',
'preamble' => 'text',
),
self::CONFIG_KEY_SCHEMA => array(
'key_creator' => array(
'columns' => array('creatorPHID', 'dateModified'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorLegalpadDocumentPHIDType::TYPECONST);
}
public function getDocumentBody() {
return $this->assertAttached($this->documentBody);
}
public function attachDocumentBody(LegalpadDocumentBody $body) {
$this->documentBody = $body;
return $this;
}
public function getContributors() {
return $this->assertAttached($this->contributors);
}
public function attachContributors(array $contributors) {
$this->contributors = $contributors;
return $this;
}
public function getSignatures() {
return $this->assertAttached($this->signatures);
}
public function attachSignatures(array $signatures) {
$this->signatures = $signatures;
return $this;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'L'.$this->getID();
}
public function getUserSignature($phid) {
return $this->assertAttachedKey($this->userSignatures, $phid);
}
public function attachUserSignature(
$user_phid,
LegalpadDocumentSignature $signature = null) {
$this->userSignatures[$user_phid] = $signature;
return $this;
}
public static function getSignatureTypeMap() {
return array(
self::SIGNATURE_TYPE_INDIVIDUAL => pht('Individuals'),
self::SIGNATURE_TYPE_CORPORATION => pht('Corporations'),
);
}
public function getSignatureTypeName() {
$type = $this->getSignatureType();
return idx(self::getSignatureTypeMap(), $type, $type);
}
public function getSignatureTypeIcon() {
$type = $this->getSignatureType();
$map = array(
self::SIGNATURE_TYPE_INDIVIDUAL => 'fa-user grey',
self::SIGNATURE_TYPE_CORPORATION => 'fa-building-o grey',
);
return idx($map, $type, 'fa-user grey');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->creatorPHID == $phid);
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$policy = $this->viewPolicy;
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$policy = $this->editPolicy;
break;
default:
$policy = PhabricatorPolicies::POLICY_NOONE;
break;
}
return $policy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getCreatorPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'The author of a document can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new LegalpadDocumentEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new LegalpadTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$bodies = id(new LegalpadDocumentBody())->loadAllWhere(
'documentPHID = %s',
$this->getPHID());
foreach ($bodies as $body) {
$body->delete();
}
$signatures = id(new LegalpadDocumentSignature())->loadAllWhere(
'documentPHID = %s',
$this->getPHID());
foreach ($signatures as $signature) {
$signature->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php
index f5122dd687..34ecfb26c1 100644
--- a/src/applications/macro/storage/PhabricatorFileImageMacro.php
+++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php
@@ -1,136 +1,143 @@
<?php
final class PhabricatorFileImageMacro extends PhabricatorFileDAO
implements
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface {
protected $authorPHID;
protected $filePHID;
protected $name;
protected $isDisabled = 0;
protected $audioPHID;
protected $audioBehavior = self::AUDIO_BEHAVIOR_NONE;
protected $mailKey;
private $file = self::ATTACHABLE;
private $audio = self::ATTACHABLE;
const AUDIO_BEHAVIOR_NONE = 'audio:none';
const AUDIO_BEHAVIOR_ONCE = 'audio:once';
const AUDIO_BEHAVIOR_LOOP = 'audio:loop';
public function attachFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function getFile() {
return $this->assertAttached($this->file);
}
public function attachAudio(PhabricatorFile $audio = null) {
$this->audio = $audio;
return $this;
}
public function getAudio() {
return $this->assertAttached($this->audio);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'authorPHID' => 'phid?',
'isDisabled' => 'bool',
'audioPHID' => 'phid?',
'audioBehavior' => 'text64',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'name' => array(
'columns' => array('name'),
'unique' => true,
),
'key_disabled' => array(
'columns' => array('isDisabled'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorMacroMacroPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorMacroEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorMacroTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index 6661b13f97..1ffe915b8a 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,377 +1,384 @@
<?php
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorMentionableInterface,
PhrequentTrackableInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $authorPHID;
protected $ownerPHID;
protected $ccPHIDs = array();
protected $status;
protected $priority;
protected $subpriority = 0;
protected $title = '';
protected $originalTitle = '';
protected $description = '';
protected $originalEmailSource;
protected $mailKey;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $attached = array();
protected $projectPHIDs = array();
private $subscribersNeedUpdate;
protected $ownerOrdering;
private $groupByProjectPHID = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $edgeProjectPHIDs = self::ATTACHABLE;
public static function initializeNewTask(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
$view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY);
return id(new ManiphestTask())
->setStatus(ManiphestTaskStatus::getDefaultStatus())
->setPriority(ManiphestTaskPriority::getDefaultPriority())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->attachProjectPHIDs(array());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'ccPHIDs' => self::SERIALIZATION_JSON,
'attached' => self::SERIALIZATION_JSON,
'projectPHIDs' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
'status' => 'text12',
'priority' => 'uint32',
'title' => 'sort',
'originalTitle' => 'text',
'description' => 'text',
'mailKey' => 'bytes20',
'ownerOrdering' => 'text64?',
'originalEmailSource' => 'text255?',
'subpriority' => 'double',
// T6203/NULLABILITY
// This should not be nullable. It's going away soon anyway.
'ccPHIDs' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'priority' => array(
'columns' => array('priority', 'status'),
),
'status' => array(
'columns' => array('status'),
),
'ownerPHID' => array(
'columns' => array('ownerPHID', 'status'),
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'ownerOrdering' => array(
'columns' => array('ownerOrdering'),
),
'priority_2' => array(
'columns' => array('priority', 'subpriority'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_dateModified' => array(
'columns' => array('dateModified'),
),
'key_title' => array(
'columns' => array('title(64)'),
),
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST);
}
public function getCCPHIDs() {
return array_values(nonempty($this->ccPHIDs, array()));
}
public function getProjectPHIDs() {
return $this->assertAttached($this->edgeProjectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->edgeProjectPHIDs = $phids;
return $this;
}
public function setCCPHIDs(array $phids) {
$this->ccPHIDs = array_values($phids);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function getMonogram() {
return 'T'.$this->getID();
}
public function attachGroupByProjectPHID($phid) {
$this->groupByProjectPHID = $phid;
return $this;
}
public function getGroupByProjectPHID() {
return $this->assertAttached($this->groupByProjectPHID);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
if ($this->subscribersNeedUpdate) {
// If we've changed the subscriber PHIDs for this task, update the link
// table.
ManiphestTaskSubscriber::updateTaskSubscribers($this);
$this->subscribersNeedUpdate = false;
}
return $result;
}
public function isClosed() {
return ManiphestTaskStatus::isClosedStatus($this->getStatus());
}
public function getPrioritySortVector() {
return array(
$this->getPriority(),
-$this->getSubpriority(),
$this->getID(),
);
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "maniphest:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// The owner of a task can always view and edit it.
$owner_phid = $this->getOwnerPHID();
if ($owner_phid) {
$user_phid = $user->getPHID();
if ($user_phid == $owner_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('The owner of a task can always view and edit it.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
// TODO: Once this implements PhabricatorTransactionInterface, this
// will be handled automatically and can be removed.
$xactions = id(new ManiphestTransaction())->loadAllWhere(
'objectPHID = %s',
$this->getPHID());
foreach ($xactions as $xaction) {
$engine->destroyObject($xaction);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ManiphestTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ManiphestTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
}
diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php
index 0bf955a875..ecf2f7bba8 100644
--- a/src/applications/nuance/storage/NuanceSource.php
+++ b/src/applications/nuance/storage/NuanceSource.php
@@ -1,110 +1,117 @@
<?php
final class NuanceSource extends NuanceDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
protected $type;
protected $data;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'data' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255?',
'type' => 'text32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('type', 'dateModified'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(NuanceSourcePHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getURI() {
return '/nuance/source/view/'.$this->getID().'/';
}
public static function initializeNewSource(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorNuanceApplication'))
->executeOne();
$view_policy = $app->getPolicy(
NuanceSourceDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
NuanceSourceDefaultEditCapability::CAPABILITY);
$definitions = NuanceSourceDefinition::getAllDefinitions();
$lucky_definition = head($definitions);
return id(new NuanceSource())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setType($lucky_definition->getSourceTypeConstant());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new NuanceSourceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new NuanceSourceTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php
index ae1fe9d49a..976a379f1a 100644
--- a/src/applications/passphrase/storage/PassphraseCredential.php
+++ b/src/applications/passphrase/storage/PassphraseCredential.php
@@ -1,144 +1,151 @@
<?php
final class PassphraseCredential extends PassphraseDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $credentialType;
protected $providesType;
protected $viewPolicy;
protected $editPolicy;
protected $description;
protected $username;
protected $secretID;
protected $isDestroyed;
protected $isLocked = 0;
protected $allowConduit = 0;
private $secret = self::ATTACHABLE;
public static function initializeNewCredential(PhabricatorUser $actor) {
return id(new PassphraseCredential())
->setName('')
->setUsername('')
->setDescription('')
->setIsDestroyed(0)
->setViewPolicy($actor->getPHID())
->setEditPolicy($actor->getPHID());
}
public function getMonogram() {
return 'K'.$this->getID();
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'credentialType' => 'text64',
'providesType' => 'text64',
'description' => 'text',
'username' => 'text255',
'secretID' => 'id?',
'isDestroyed' => 'bool',
'isLocked' => 'bool',
'allowConduit' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_secret' => array(
'columns' => array('secretID'),
'unique' => true,
),
'key_type' => array(
'columns' => array('credentialType'),
),
'key_provides' => array(
'columns' => array('providesType'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PassphraseCredentialPHIDType::TYPECONST);
}
public function attachSecret(PhutilOpaqueEnvelope $secret = null) {
$this->secret = $secret;
return $this;
}
public function getSecret() {
return $this->assertAttached($this->secret);
}
public function getCredentialTypeImplementation() {
$type = $this->getCredentialType();
return PassphraseCredentialType::getTypeByConstant($type);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PassphraseCredentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PassphraseCredentialTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$secrets = id(new PassphraseSecret())->loadAllWhere(
'id = %d',
$this->getSecretID());
foreach ($secrets as $secret) {
$secret->delete();
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php
index 02f37853af..a2b478a8f6 100644
--- a/src/applications/paste/storage/PhabricatorPaste.php
+++ b/src/applications/paste/storage/PhabricatorPaste.php
@@ -1,197 +1,204 @@
<?php
final class PhabricatorPaste extends PhabricatorPasteDAO
implements
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorMentionableInterface,
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $title;
protected $authorPHID;
protected $filePHID;
protected $language;
protected $parentPHID;
protected $viewPolicy;
protected $mailKey;
private $content = self::ATTACHABLE;
private $rawContent = self::ATTACHABLE;
public static function initializeNewPaste(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPasteApplication'))
->executeOne();
$view_policy = $app->getPolicy(PasteDefaultViewCapability::CAPABILITY);
return id(new PhabricatorPaste())
->setTitle('')
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy);
}
public function getURI() {
return '/P'.$this->getID();
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'language' => 'text64',
'mailKey' => 'bytes20',
'parentPHID' => 'phid?',
// T6203/NULLABILITY
// Pastes should always have a view policy.
'viewPolicy' => 'policy?',
),
self::CONFIG_KEY_SCHEMA => array(
'parentPHID' => array(
'columns' => array('parentPHID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_language' => array(
'columns' => array('language'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPastePastePHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getFullName() {
$title = $this->getTitle();
if (!$title) {
$title = pht('(An Untitled Masterwork)');
}
return 'P'.$this->getID().' '.$title;
}
public function getContent() {
return $this->assertAttached($this->content);
}
public function attachContent($content) {
$this->content = $content;
return $this;
}
public function getRawContent() {
return $this->assertAttached($this->rawContent);
}
public function attachRawContent($raw_content) {
$this->rawContent = $raw_content;
return $this;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return $this->viewPolicy;
}
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht('The author of a paste can always view and edit it.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
if ($this->filePHID) {
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->filePHID))
->executeOne();
if ($file) {
$engine->destroyObject($file);
}
}
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorPasteEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorPasteTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
}
diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php
index 845bb27b63..7a356b8c44 100644
--- a/src/applications/phlux/storage/PhluxVariable.php
+++ b/src/applications/phlux/storage/PhluxVariable.php
@@ -1,80 +1,87 @@
<?php
final class PhluxVariable extends PhluxDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface {
protected $variableKey;
protected $variableValue;
protected $viewPolicy;
protected $editPolicy;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'variableValue' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'variableKey' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_key' => array(
'columns' => array('variableKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PhluxVariablePHIDType::TYPECONST);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhluxVariableEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhluxTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->viewPolicy;
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->editPolicy;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php
index e4a574d480..bea0f3ceb8 100644
--- a/src/applications/pholio/storage/PholioMock.php
+++ b/src/applications/pholio/storage/PholioMock.php
@@ -1,296 +1,303 @@
<?php
final class PholioMock extends PholioDAO
implements
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:description';
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $name;
protected $originalName;
protected $description;
protected $coverPHID;
protected $mailKey;
protected $status;
private $images = self::ATTACHABLE;
private $allImages = self::ATTACHABLE;
private $coverFile = self::ATTACHABLE;
private $tokenCount = self::ATTACHABLE;
public static function initializeNewMock(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPholioApplication'))
->executeOne();
$view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY);
return id(new PholioMock())
->setAuthorPHID($actor->getPHID())
->attachImages(array())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
public function getMonogram() {
return 'M'.$this->getID();
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'description' => 'text',
'originalName' => 'text128',
'mailKey' => 'bytes20',
'status' => 'text12',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('MOCK');
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/**
* These should be the images currently associated with the Mock.
*/
public function attachImages(array $images) {
assert_instances_of($images, 'PholioImage');
$this->images = $images;
return $this;
}
public function getImages() {
$this->assertAttached($this->images);
return $this->images;
}
/**
* These should be *all* images associated with the Mock. This includes
* images which have been removed and / or replaced from the Mock.
*/
public function attachAllImages(array $images) {
assert_instances_of($images, 'PholioImage');
$this->allImages = $images;
return $this;
}
public function getAllImages() {
$this->assertAttached($this->images);
return $this->allImages;
}
public function attachCoverFile(PhabricatorFile $file) {
$this->coverFile = $file;
return $this;
}
public function getCoverFile() {
$this->assertAttached($this->coverFile);
return $this->coverFile;
}
public function getTokenCount() {
$this->assertAttached($this->tokenCount);
return $this->tokenCount;
}
public function attachTokenCount($count) {
$this->tokenCount = $count;
return $this;
}
public function getImageHistorySet($image_id) {
$images = $this->getAllImages();
$images = mpull($images, null, 'getID');
$selected_image = $images[$image_id];
$replace_map = mpull($images, null, 'getReplacesImagePHID');
$phid_map = mpull($images, null, 'getPHID');
// find the earliest image
$image = $selected_image;
while (isset($phid_map[$image->getReplacesImagePHID()])) {
$image = $phid_map[$image->getReplacesImagePHID()];
}
// now build history moving forward
$history = array($image->getID() => $image);
while (isset($replace_map[$image->getPHID()])) {
$image = $replace_map[$image->getPHID()];
$history[$image->getID()] = $image;
}
return $history;
}
public function getStatuses() {
$options = array();
$options['open'] = pht('Open');
$options['closed'] = pht('Closed');
return $options;
}
public function isClosed() {
return ($this->getStatus() == 'closed');
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht("A mock's owner can always view and edit it.");
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return 'M:'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
if ($this->getDescription()) {
$description = $this->getDescription();
} else {
$description = pht('No Description Given');
}
return $description;
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PholioMockEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PholioTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ $timeline->setMock($this);
+ return $timeline;
+ }
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$images = id(new PholioImage())->loadAllWhere(
'mockID = %d',
$this->getID());
foreach ($images as $image) {
$image->delete();
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php
index 30f9027eea..3bb380e151 100644
--- a/src/applications/phortune/storage/PhortuneAccount.php
+++ b/src/applications/phortune/storage/PhortuneAccount.php
@@ -1,153 +1,160 @@
<?php
/**
* An account represents a purchasing entity. An account may have multiple users
* on it (e.g., several employees of a company have access to the company
* account), and a user may have several accounts (e.g., a company account and
* a personal account).
*/
final class PhortuneAccount extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
private $memberPHIDs = self::ATTACHABLE;
public static function initializeNewAccount(PhabricatorUser $actor) {
$account = id(new PhortuneAccount());
$account->memberPHIDs = array();
return $account;
}
public static function createNewAccount(
PhabricatorUser $actor,
PhabricatorContentSource $content_source) {
$account = PhortuneAccount::initializeNewAccount($actor);
$xactions = array();
$xactions[] = id(new PhortuneAccountTransaction())
->setTransactionType(PhortuneAccountTransaction::TYPE_NAME)
->setNewValue(pht('Personal Account'));
$xactions[] = id(new PhortuneAccountTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhortuneAccountHasMemberEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array($actor->getPHID() => $actor->getPHID()),
));
$editor = id(new PhortuneAccountEditor())
->setActor($actor)
->setContentSource($content_source);
// We create an account for you the first time you visit Phortune.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$editor->applyTransactions($account, $xactions);
unset($unguarded);
return $account;
}
public function newCart(
PhabricatorUser $actor,
PhortuneCartImplementation $implementation,
PhortuneMerchant $merchant) {
$cart = PhortuneCart::initializeNewCart($actor, $this, $merchant);
$cart->setCartClass(get_class($implementation));
$cart->attachImplementation($implementation);
$implementation->willCreateCart($actor, $cart);
return $cart->save();
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneAccountPHIDType::TYPECONST);
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneAccountEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneAccountTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// Accounts are technically visible to all users, because merchant
// controllers need to be able to see accounts in order to process
// orders. We lock things down more tightly at the application level.
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getPHID() === null) {
// Allow a user to create an account for themselves.
return PhabricatorPolicies::POLICY_USER;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$members = array_fuse($this->getMemberPHIDs());
return isset($members[$viewer->getPHID()]);
}
public function describeAutomaticCapability($capability) {
return pht('Members of an account can always view and edit it.');
}
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index b90223253b..31eb3a4b88 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,663 +1,670 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold';
const STATUS_REVIEW = 'cart:review';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
protected $mailKey;
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
PhortuneAccount $account,
PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
->setMerchantPHID($merchant->getPHID());
$cart->account = $account;
$cart->purchases = array();
return $cart;
}
public function newPurchase(
PhabricatorUser $actor,
PhortuneProduct $product) {
$purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->save();
$this->purchases[] = $purchase;
return $purchase;
}
public static function getStatusNameMap() {
return array(
self::STATUS_BUILDING => pht('Building'),
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_REVIEW => pht('Review'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, $status);
}
public function activateCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_BUILDING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call willApplyCharge().',
$copy->getStatus()));
}
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
return $this;
}
public function willApplyCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortunePaymentMethod $method = null) {
$account = $this->getAccount();
$charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($account->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setAmountAsCurrency($this->getTotalPriceAsCurrency());
if ($method) {
$charge->setPaymentMethodPHID($method->getPHID());
}
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_READY) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call willApplyCharge(), '.
'expected "%s".',
$copy->getStatus(),
self::STATUS_READY));
}
$charge->save();
$this->setStatus(PhortuneCart::STATUS_PURCHASING)->save();
$this->endReadLocking();
$this->saveTransaction();
return $charge;
}
public function didHoldCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_HOLD);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didHoldCharge(), '.
'expected "%s".',
$copy->getStatus(),
self::STATUS_PURCHASING));
}
$charge->save();
$this->setStatus(self::STATUS_HOLD)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
}
public function didApplyCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didApplyCharge().',
$copy->getStatus()));
}
$charge->save();
$this->setStatus(self::STATUS_CHARGED)->save();
$this->endReadLocking();
$this->saveTransaction();
// TODO: Perform purchase review. Here, we would apply rules to determine
// whether the charge needs manual review (maybe making the decision via
// Herald, configuration, or by examining provider fraud data). For now,
// always require review.
$needs_review = true;
if ($needs_review) {
$this->willReviewCart();
} else {
$this->didReviewCart();
}
return $this;
}
public function willReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call willReviewCart()!',
$copy->getStatus()));
}
$this->setStatus(self::STATUS_REVIEW)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
return $this;
}
public function didReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED) &&
($copy->getStatus() !== self::STATUS_REVIEW)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didReviewCart()!',
$copy->getStatus()));
}
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didFailCharge().',
$copy->getStatus()));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortuneCharge $charge,
PhortuneCurrency $amount) {
if (!$amount->isPositive()) {
throw new Exception(
pht('Trying to refund nonpositive amount of money!'));
}
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
throw new Exception(
pht('Trying to refund more money than remaining on charge!'));
}
if ($charge->getRefundedChargePHID()) {
throw new Exception(
pht('Trying to refund a refund!'));
}
if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($copy->getRefundingPHID() !== null) {
throw new Exception(
pht('Trying to refund a charge which is already refunding!'));
}
$refund_charge->save();
$charge->setRefundingPHID($refund_charge->getPHID());
$charge->save();
$charge->endReadLocking();
$charge->saveTransaction();
return $refund_charge;
}
public function didRefundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
// NOTE: There's some trickiness here to get the signs right. Both
// these values are positive but the refund has a negative value.
$total_refunded = $charge
->getAmountRefundedAsCurrency()
->add($refund->getAmountAsCurrency()->negate());
$charge->setAmountRefundedAsCurrency($total_refunded);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
$amount = $refund->getAmountAsCurrency()->negate();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didRefundProduct($purchase, $amount);
}
return $this;
}
public function didFailRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
}
private function recordCartTransaction($type) {
$omnipotent_user = PhabricatorUser::getOmnipotentUser();
$phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType($type)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_PHORTUNE,
array());
$editor = id(new PhortuneCartEditor())
->setActor($omnipotent_user)
->setActingAsPHID($phortune_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($this, $xactions);
}
public function getName() {
return $this->getImplementation()->getName($this);
}
public function getDoneURI() {
return $this->getImplementation()->getDoneURI($this);
}
public function getDoneActionName() {
return $this->getImplementation()->getDoneActionName($this);
}
public function getCancelURI() {
return $this->getImplementation()->getCancelURI($this);
}
public function getDetailURI() {
return '/phortune/cart/'.$this->getID().'/';
}
public function getCheckoutURI() {
return '/phortune/cart/'.$this->getID().'/checkout/';
}
public function canCancelOrder() {
try {
$this->assertCanCancelOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function canRefundOrder() {
try {
$this->assertCanRefundOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanCancelOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be cancelled because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be cancelled because it has not been placed.'));
}
return $this->getImplementation()->assertCanCancelOrder($this);
}
public function assertCanRefundOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be refunded because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be refunded because it has not been placed.'));
}
return $this->getImplementation()->assertCanRefundOrder($this);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'cartClass' => 'text128',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneCartPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function attachPurchases(array $purchases) {
assert_instances_of($purchases, 'PhortunePurchase');
$this->purchases = $purchases;
return $this;
}
public function getPurchases() {
return $this->assertAttached($this->purchases);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function attachImplementation(
PhortuneCartImplementation $implementation) {
$this->implementation = $implementation;
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function getTotalPriceAsCurrency() {
$prices = array();
foreach ($this->getPurchases() as $purchase) {
$prices[] = $purchase->getTotalPriceAsCurrency();
}
return PhortuneCurrency::newFromList($prices);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneCartEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneCartTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Both view and edit use the account's edit policy. We punch a hole
// through this for merchants, below.
return $this
->getAccount()
->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
return true;
}
// If the viewer controls the merchant this order was placed with, they
// can view the order.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_admin) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Orders inherit the policies of the associated account.'),
pht('The merchant you placed an order with can review and manage it.'),
);
}
}
diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php
index 05dbb00ebb..ce4cd4466d 100644
--- a/src/applications/phortune/storage/PhortuneMerchant.php
+++ b/src/applications/phortune/storage/PhortuneMerchant.php
@@ -1,93 +1,100 @@
<?php
final class PhortuneMerchant extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
protected $viewPolicy;
protected $description;
private $memberPHIDs = self::ATTACHABLE;
public static function initializeNewMerchant(PhabricatorUser $actor) {
return id(new PhortuneMerchant())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->attachMemberPHIDs(array());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'description' => 'text',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneMerchantPHIDType::TYPECONST);
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function attachMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneMerchantEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneMerchantTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$members = array_fuse($this->getMemberPHIDs());
if (isset($members[$viewer->getPHID()])) {
return true;
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht("A merchant's members an always view and edit it.");
}
}
diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php
index fe1ed78a48..a950852795 100644
--- a/src/applications/phriction/storage/PhrictionDocument.php
+++ b/src/applications/phriction/storage/PhrictionDocument.php
@@ -1,249 +1,255 @@
<?php
final class PhrictionDocument extends PhrictionDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $slug;
protected $depth;
protected $contentID;
protected $status;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
private $contentObject = self::ATTACHABLE;
private $ancestors = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'slug' => 'sort128',
'depth' => 'uint32',
'contentID' => 'id?',
'status' => 'uint32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'slug' => array(
'columns' => array('slug'),
'unique' => true,
),
'depth' => array(
'columns' => array('depth', 'slug'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhrictionDocumentPHIDType::TYPECONST);
}
public static function initializeNewDocument(PhabricatorUser $actor, $slug) {
$document = new PhrictionDocument();
$document->setSlug($slug);
$content = new PhrictionContent();
$content->setSlug($slug);
$default_title = PhabricatorSlug::getDefaultTitle($slug);
$content->setTitle($default_title);
$document->attachContent($content);
$parent_doc = null;
$ancestral_slugs = PhabricatorSlug::getAncestry($slug);
if ($ancestral_slugs) {
$parent = end($ancestral_slugs);
$parent_doc = id(new PhrictionDocumentQuery())
->setViewer($actor)
->withSlugs(array($parent))
->executeOne();
}
if ($parent_doc) {
$document->setViewPolicy($parent_doc->getViewPolicy());
$document->setEditPolicy($parent_doc->getEditPolicy());
} else {
$default_view_policy = PhabricatorPolicies::getMostOpenPolicy();
$document->setViewPolicy($default_view_policy);
$document->setEditPolicy(PhabricatorPolicies::POLICY_USER);
}
return $document;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public static function getSlugURI($slug, $type = 'document') {
static $types = array(
'document' => '/w/',
'history' => '/phriction/history/',
);
if (empty($types[$type])) {
throw new Exception("Unknown URI type '{$type}'!");
}
$prefix = $types[$type];
if ($slug == '/') {
return $prefix;
} else {
// NOTE: The effect here is to escape non-latin characters, since modern
// browsers deal with escaped UTF8 characters in a reasonable way (showing
// the user a readable URI) but older programs may not.
$slug = phutil_escape_uri($slug);
return $prefix.$slug;
}
}
public function setSlug($slug) {
$this->slug = PhabricatorSlug::normalize($slug);
$this->depth = PhabricatorSlug::getDepth($slug);
return $this;
}
public function attachContent(PhrictionContent $content) {
$this->contentObject = $content;
return $this;
}
public function getContent() {
return $this->assertAttached($this->contentObject);
}
public function getAncestors() {
return $this->ancestors;
}
public function getAncestor($slug) {
return $this->assertAttachedKey($this->ancestors, $slug);
}
public function attachAncestor($slug, $ancestor) {
$this->ancestors[$slug] = $ancestor;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht(
'To view a wiki document, you must also be able to view all '.
'of its parents.');
case PhabricatorPolicyCapability::CAN_EDIT:
return pht(
'To edit a wiki document, you must also be able to view all '.
'of its parents.');
}
return null;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhrictionTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhrictionTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$contents = id(new PhrictionContent())->loadAllWhere(
'documentID = %d',
$this->getID());
foreach ($contents as $content) {
$content->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php
index 6cdf079d45..7e6cdbd19e 100644
--- a/src/applications/ponder/storage/PonderAnswer.php
+++ b/src/applications/ponder/storage/PonderAnswer.php
@@ -1,254 +1,261 @@
<?php
final class PonderAnswer extends PonderDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorMarkupInterface,
PonderVotableInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $authorPHID;
protected $questionID;
protected $content;
protected $contentSource;
protected $voteCount;
private $vote;
private $question = self::ATTACHABLE;
private $comments;
private $userVotes = array();
public function attachQuestion(PonderQuestion $question = null) {
$this->question = $question;
return $this;
}
public function getQuestion() {
return $this->assertAttached($this->question);
}
public function getURI() {
return '/Q'.$this->getQuestionID().'#A'.$this->getID();
}
public function setUserVote($vote) {
$this->vote = $vote['data'];
if (!$this->vote) {
$this->vote = PonderVote::VOTE_NONE;
}
return $this;
}
public function attachUserVote($user_phid, $vote) {
$this->vote = $vote;
return $this;
}
public function getUserVote() {
return $this->vote;
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'voteCount' => 'sint32',
'content' => 'text',
// T6203/NULLABILITY
// This should always exist.
'contentSource' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'key_oneanswerperquestion' => array(
'columns' => array('questionID', 'authorPHID'),
'unique' => true,
),
'questionID' => array(
'columns' => array('questionID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PonderAnswerPHIDType::TYPECONST);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PonderAnswerEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PonderAnswerTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:A{$id}:{$field}:{$hash}";
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
// votable interface
public function getUserVoteEdgeType() {
return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER;
}
public function getVotablePHID() {
return $this->getPHID();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getQuestion()->getPolicy($capability);
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->getAuthorPHID() == $viewer->getPHID()) {
return true;
}
return $this->getQuestion()->hasAutomaticCapability(
$capability,
$viewer);
case PhabricatorPolicyCapability::CAN_EDIT:
return ($this->getAuthorPHID() == $viewer->getPHID());
}
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The author of an answer can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'The user who asks a question can always view the answers.');
break;
}
return $out;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php
index abeda5e98c..6237f48c9a 100644
--- a/src/applications/ponder/storage/PonderQuestion.php
+++ b/src/applications/ponder/storage/PonderQuestion.php
@@ -1,295 +1,302 @@
<?php
final class PonderQuestion extends PonderDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorMarkupInterface,
PonderVotableInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $title;
protected $phid;
protected $authorPHID;
protected $status;
protected $content;
protected $contentSource;
protected $voteCount;
protected $answerCount;
protected $heat;
protected $mailKey;
private $answers;
private $vote;
private $comments;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'voteCount' => 'sint32',
'status' => 'uint32',
'content' => 'text',
'heat' => 'double',
'answerCount' => 'uint32',
'mailKey' => 'bytes20',
// T6203/NULLABILITY
// This should always exist.
'contentSource' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'heat' => array(
'columns' => array('heat'),
),
'status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function attachVotes($user_phid) {
$qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID());
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($user_phid))
->withDestinationPHIDs($qa_phids)
->withEdgeTypes(
array(
PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION,
PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER,
))
->needEdgeData(true)
->execute();
$question_edge =
$edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION];
$answer_edges =
$edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER];
$edges = null;
$this->setUserVote(idx($question_edge, $this->getPHID()));
foreach ($this->answers as $answer) {
$answer->setUserVote(idx($answer_edges, $answer->getPHID()));
}
}
public function setUserVote($vote) {
$this->vote = $vote['data'];
if (!$this->vote) {
$this->vote = PonderVote::VOTE_NONE;
}
return $this;
}
public function attachUserVote($user_phid, $vote) {
$this->vote = $vote;
return $this;
}
public function getUserVote() {
return $this->vote;
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function attachAnswers(array $answers) {
assert_instances_of($answers, 'PonderAnswer');
$this->answers = $answers;
return $this;
}
public function getAnswers() {
return $this->answers;
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PonderQuestionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PonderQuestionTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:Q{$id}:{$field}:{$hash}";
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
// votable interface
public function getUserVoteEdgeType() {
return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION;
}
public function getVotablePHID() {
return $this->getPHID();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getOriginalTitle() {
// TODO: Make this actually save/return the original title.
return $this->getTitle();
}
public function getFullTitle() {
$id = $this->getID();
$title = $this->getTitle();
return "Q{$id}: {$title}";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
$policy = PhabricatorPolicies::POLICY_NOONE;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$policy = PhabricatorPolicies::POLICY_USER;
break;
}
return $policy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'The user who asked a question can always view and edit it.');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$answers = id(new PonderAnswer())->loadAllWhere(
'questionID = %d',
$this->getID());
foreach ($answers as $answer) {
$engine->destroyObject($answer);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index 42e2b578e7..0274bd2930 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,402 +1,409 @@
<?php
final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $subprojectPHIDs = array();
protected $phrictionSlug;
protected $profileImagePHID;
protected $icon;
protected $color;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
protected $isMembershipLocked;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $slugs = self::ATTACHABLE;
const DEFAULT_ICON = 'fa-briefcase';
const DEFAULT_COLOR = 'blue';
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
public static function initializeNewProject(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorProjectApplication'))
->executeOne();
$view_policy = $app->getPolicy(
ProjectDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
ProjectDefaultEditCapability::CAPABILITY);
$join_policy = $app->getPolicy(
ProjectDefaultJoinCapability::CAPABILITY);
return id(new PhabricatorProject())
->setName('')
->setAuthorPHID($actor->getPHID())
->setIcon(self::DEFAULT_ICON)
->setColor(self::DEFAULT_COLOR)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setJoinPolicy($join_policy)
->setIsMembershipLocked(0)
->attachMemberPHIDs(array())
->attachSlugs(array());
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
break;
case PhabricatorPolicyCapability::CAN_JOIN:
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Members of a project can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht('Users who can edit a project can always join it.');
}
return null;
}
public function isUserMember($user_phid) {
if ($this->memberPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->memberPHIDs);
}
return $this->assertAttachedKey($this->sparseMembers, $user_phid);
}
public function setIsUserMember($user_phid, $is_member) {
if ($this->sparseMembers === self::ATTACHABLE) {
$this->sparseMembers = array();
}
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'subprojectPHIDs' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'status' => 'text32',
'phrictionSlug' => 'text128?',
'isMembershipLocked' => 'bool',
'profileImagePHID' => 'phid?',
'icon' => 'text32',
'color' => 'text32',
// T6203/NULLABILITY
// These are definitely wrong and should always exist.
'editPolicy' => 'policy?',
'viewPolicy' => 'policy?',
'joinPolicy' => 'policy?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'key_icon' => array(
'columns' => array('icon'),
),
'key_color' => array(
'columns' => array('color'),
),
'phrictionSlug' => array(
'columns' => array('phrictionSlug'),
'unique' => true,
),
'name' => array(
'columns' => array('name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectProjectPHIDType::TYPECONST);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function setPhrictionSlug($slug) {
// NOTE: We're doing a little magic here and stripping out '/' so that
// project pages always appear at top level under projects/ even if the
// display name is "Hack / Slash" or similar (it will become
// 'hack_slash' instead of 'hack/slash').
$slug = str_replace('/', ' ', $slug);
$slug = PhabricatorSlug::normalize($slug);
$this->phrictionSlug = $slug;
return $this;
}
public function getFullPhrictionSlug() {
$slug = $this->getPhrictionSlug();
return 'projects/'.$slug;
}
// TODO - once we sever project => phriction automagicalness,
// migrate getPhrictionSlug to have no trailing slash and be called
// getPrimarySlug
public function getPrimarySlug() {
$slug = $this->getPhrictionSlug();
return rtrim($slug, '/');
}
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function isUserWatcher($user_phid) {
if ($this->watcherPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->watcherPHIDs);
}
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
}
public function setIsUserWatcher($user_phid, $is_watcher) {
if ($this->sparseWatchers === self::ATTACHABLE) {
$this->sparseWatchers = array();
}
$this->sparseWatchers[$user_phid] = $is_watcher;
return $this;
}
public function attachWatcherPHIDs(array $phids) {
$this->watcherPHIDs = $phids;
return $this;
}
public function getWatcherPHIDs() {
return $this->assertAttached($this->watcherPHIDs);
}
public function attachSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function getSlugs() {
return $this->assertAttached($this->slugs);
}
public function getColor() {
if ($this->isArchived()) {
return PHUITagView::COLOR_DISABLED;
}
return $this->color;
}
public function save() {
$this->openTransaction();
$result = parent::save();
$this->updateDatasourceTokens();
$this->saveTransaction();
return $result;
}
public function updateDatasourceTokens() {
$table = self::TABLE_DATASOURCE_TOKEN;
$conn_w = $this->establishConnection('w');
$id = $this->getID();
$slugs = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE projectPHID = %s',
id(new PhabricatorProjectSlug())->getTableName(),
$this->getPHID());
$all_strings = ipull($slugs, 'slug');
$all_strings[] = $this->getName();
$all_strings = implode(' ', $all_strings);
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
}
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE projectID = %d',
$table,
$id);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (projectID, token) VALUES %Q',
$table,
$chunk);
}
$this->saveTransaction();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
public function shouldShowSubscribersProperty() {
return false;
}
public function shouldAllowSubscription($phid) {
return $this->isUserMember($phid) &&
!$this->isUserWatcher($phid);
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$columns = id(new PhabricatorProjectColumn())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($columns as $column) {
$engine->destroyObject($column);
}
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($slugs as $slug) {
$slug->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php
index 12e89713c1..de6a1cc35d 100644
--- a/src/applications/project/storage/PhabricatorProjectColumn.php
+++ b/src/applications/project/storage/PhabricatorProjectColumn.php
@@ -1,190 +1,197 @@
<?php
final class PhabricatorProjectColumn
extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const STATUS_ACTIVE = 0;
const STATUS_HIDDEN = 1;
const DEFAULT_ORDER = 'natural';
const ORDER_NATURAL = 'natural';
const ORDER_PRIORITY = 'priority';
protected $name;
protected $status;
protected $projectPHID;
protected $sequence;
protected $properties = array();
private $project = self::ATTACHABLE;
public static function initializeNewColumn(PhabricatorUser $user) {
return id(new PhabricatorProjectColumn())
->setName('')
->setStatus(self::STATUS_ACTIVE);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'status' => 'uint32',
'sequence' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('projectPHID', 'status', 'sequence'),
),
'key_sequence' => array(
'columns' => array('projectPHID', 'sequence'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectColumnPHIDType::TYPECONST);
}
public function attachProject(PhabricatorProject $project) {
$this->project = $project;
return $this;
}
public function getProject() {
return $this->assertAttached($this->project);
}
public function isDefaultColumn() {
return (bool)$this->getProperty('isDefault');
}
public function isHidden() {
return ($this->getStatus() == self::STATUS_HIDDEN);
}
public function getDisplayName() {
$name = $this->getName();
if (strlen($name)) {
return $name;
}
if ($this->isDefaultColumn()) {
return pht('Backlog');
}
return pht('Unnamed Column');
}
public function getDisplayType() {
if ($this->isDefaultColumn()) {
return pht('(Default)');
}
if ($this->isHidden()) {
return pht('(Hidden)');
}
return null;
}
public function getHeaderIcon() {
$icon = null;
if ($this->isHidden()) {
$icon = 'fa-eye-slash';
$text = pht('Hidden');
}
if ($icon) {
return id(new PHUIIconView())
->setIconFont($icon)
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $text,
));;
}
return null;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getPointLimit() {
return $this->getProperty('pointLimit');
}
public function setPointLimit($limit) {
$this->setProperty('pointLimit', $limit);
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectColumnTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectColumnTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getProject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getProject()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Users must be able to see a project to see its board.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php
index bc83b5e0a2..c4f4fabc04 100644
--- a/src/applications/releeph/storage/ReleephBranch.php
+++ b/src/applications/releeph/storage/ReleephBranch.php
@@ -1,192 +1,199 @@
<?php
final class ReleephBranch extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $releephProjectID;
protected $isActive;
protected $createdByUserPHID;
// The immutable name of this branch ('releases/foo-2013.01.24')
protected $name;
protected $basename;
// The symbolic name of this branch (LATEST, PRODUCTION, RC, ...)
// See SYMBOLIC_NAME_NOTE below
protected $symbolicName;
// Where to cut the branch
protected $cutPointCommitPHID;
protected $details = array();
private $project = self::ATTACHABLE;
private $cutPointCommit = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'basename' => 'text64',
'isActive' => 'bool',
'symbolicName' => 'text64?',
'name' => 'text128',
),
self::CONFIG_KEY_SCHEMA => array(
'releephProjectID' => array(
'columns' => array('releephProjectID', 'symbolicName'),
'unique' => true,
),
'releephProjectID_2' => array(
'columns' => array('releephProjectID', 'basename'),
'unique' => true,
),
'releephProjectID_name' => array(
'columns' => array('releephProjectID', 'name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ReleephBranchPHIDType::TYPECONST);
}
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function willWriteData(array &$data) {
// If symbolicName is omitted, set it to the basename.
//
// This means that we can enforce symbolicName as a UNIQUE column in the
// DB. We'll interpret symbolicName === basename as meaning "no symbolic
// name".
//
// SYMBOLIC_NAME_NOTE
if (!$data['symbolicName']) {
$data['symbolicName'] = $data['basename'];
}
parent::willWriteData($data);
}
public function getSymbolicName() {
// See SYMBOLIC_NAME_NOTE above for why this is needed
if ($this->symbolicName == $this->getBasename()) {
return '';
}
return $this->symbolicName;
}
public function setSymbolicName($name) {
if ($name) {
parent::setSymbolicName($name);
} else {
parent::setSymbolicName($this->getBasename());
}
return $this;
}
public function getDisplayName() {
if ($sn = $this->getSymbolicName()) {
return $sn;
}
return $this->getBasename();
}
public function getDisplayNameWithDetail() {
$n = $this->getBasename();
if ($sn = $this->getSymbolicName()) {
return "{$sn} ({$n})";
} else {
return $n;
}
}
public function getURI($path = null) {
$components = array(
'/releeph/branch',
$this->getID(),
$path,
);
return implode('/', $components);
}
public function isActive() {
return $this->getIsActive();
}
public function attachProject(ReleephProject $project) {
$this->project = $project;
return $this;
}
public function getProject() {
return $this->assertAttached($this->project);
}
public function getProduct() {
return $this->getProject();
}
public function attachCutPointCommit(
PhabricatorRepositoryCommit $commit = null) {
$this->cutPointCommit = $commit;
return $this;
}
public function getCutPointCommit() {
return $this->assertAttached($this->cutPointCommit);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephBranchEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ReleephBranchTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return $this->getProduct()->getCapabilities();
}
public function getPolicy($capability) {
return $this->getProduct()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getProduct()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Release branches have the same policies as the product they are a '.
'part of.');
}
}
diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php
index b92e78afaa..d5270f4024 100644
--- a/src/applications/releeph/storage/ReleephProject.php
+++ b/src/applications/releeph/storage/ReleephProject.php
@@ -1,166 +1,173 @@
<?php
final class ReleephProject extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const DEFAULT_BRANCH_NAMESPACE = 'releeph-releases';
const SYSTEM_AGENT_USERNAME_PREFIX = 'releeph-agent-';
protected $name;
// Specifying the place to pick from is a requirement for svn, though not
// for git. It's always useful though for reasoning about what revs have
// been picked and which haven't.
protected $trunkBranch;
protected $repositoryPHID;
protected $isActive;
protected $createdByUserPHID;
protected $arcanistProjectID;
protected $details = array();
private $repository = self::ATTACHABLE;
private $arcanistProject = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'trunkBranch' => 'text255',
'isActive' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'projectName' => array(
'columns' => array('name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ReleephProductPHIDType::TYPECONST);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function getURI($path = null) {
$components = array(
'/releeph/product',
$this->getID(),
$path,
);
return implode('/', $components);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getArcanistProject() {
return $this->assertAttached($this->arcanistProject);
}
public function attachArcanistProject(
PhabricatorRepositoryArcanistProject $arcanist_project = null) {
$this->arcanistProject = $arcanist_project;
return $this;
}
public function getPushers() {
return $this->getDetail('pushers', array());
}
public function isPusher(PhabricatorUser $user) {
// TODO Deprecate this once `isPusher` is out of the Facebook codebase.
return $this->isAuthoritative($user);
}
public function isAuthoritative(PhabricatorUser $user) {
return $this->isAuthoritativePHID($user->getPHID());
}
public function isAuthoritativePHID($phid) {
$pushers = $this->getPushers();
if (!$pushers) {
return true;
} else {
return in_array($phid, $pushers);
}
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function getReleephFieldSelector() {
return new ReleephDefaultFieldSelector();
}
public function isTestFile($filename) {
$test_paths = $this->getDetail('testPaths', array());
foreach ($test_paths as $test_path) {
if (preg_match($test_path, $filename)) {
return true;
}
}
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephProductEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ReleephProductTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php
index a29bdee3f7..22a6270a2b 100644
--- a/src/applications/releeph/storage/ReleephRequest.php
+++ b/src/applications/releeph/storage/ReleephRequest.php
@@ -1,363 +1,370 @@
<?php
final class ReleephRequest extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $branchID;
protected $requestUserPHID;
protected $details = array();
protected $userIntents = array();
protected $inBranch;
protected $pickStatus;
protected $mailKey;
/**
* The object which is being requested. Normally this is a commit, but it
* might also be a revision. In the future, it could be a repository branch
* or an external object (like a GitHub pull request).
*/
protected $requestedObjectPHID;
// Information about the thing being requested
protected $requestCommitPHID;
// Information about the last commit to the releeph branch
protected $commitIdentifier;
protected $commitPHID;
private $customFields = self::ATTACHABLE;
private $branch = self::ATTACHABLE;
private $requestedObject = self::ATTACHABLE;
/* -( Constants and helper methods )--------------------------------------- */
const INTENT_WANT = 'want';
const INTENT_PASS = 'pass';
const PICK_PENDING = 1; // old
const PICK_FAILED = 2;
const PICK_OK = 3;
const PICK_MANUAL = 4; // old
const REVERT_OK = 5;
const REVERT_FAILED = 6;
public function shouldBeInBranch() {
return
$this->getPusherIntent() == self::INTENT_WANT &&
/**
* We use "!= pass" instead of "== want" in case the requestor intent is
* not present. In other words, only revert if the requestor explicitly
* passed.
*/
$this->getRequestorIntent() != self::INTENT_PASS;
}
/**
* Will return INTENT_WANT if any pusher wants this request, and no pusher
* passes on this request.
*/
public function getPusherIntent() {
$product = $this->getBranch()->getProduct();
if (!$product->getPushers()) {
return self::INTENT_WANT;
}
$found_pusher_want = false;
foreach ($this->userIntents as $phid => $intent) {
if ($product->isAuthoritativePHID($phid)) {
if ($intent == self::INTENT_PASS) {
return self::INTENT_PASS;
}
$found_pusher_want = true;
}
}
if ($found_pusher_want) {
return self::INTENT_WANT;
} else {
return null;
}
}
public function getRequestorIntent() {
return idx($this->userIntents, $this->requestUserPHID);
}
public function getStatus() {
return $this->calculateStatus();
}
public function getMonogram() {
return 'Y'.$this->getID();
}
public function getBranch() {
return $this->assertAttached($this->branch);
}
public function attachBranch(ReleephBranch $branch) {
$this->branch = $branch;
return $this;
}
public function getRequestedObject() {
return $this->assertAttached($this->requestedObject);
}
public function attachRequestedObject($object) {
$this->requestedObject = $object;
return $this;
}
private function calculateStatus() {
if ($this->shouldBeInBranch()) {
if ($this->getInBranch()) {
return ReleephRequestStatus::STATUS_PICKED;
} else {
return ReleephRequestStatus::STATUS_NEEDS_PICK;
}
} else {
if ($this->getInBranch()) {
return ReleephRequestStatus::STATUS_NEEDS_REVERT;
} else {
$has_been_in_branch = $this->getCommitIdentifier();
// Regardless of why we reverted something, always say reverted if it
// was once in the branch.
if ($has_been_in_branch) {
return ReleephRequestStatus::STATUS_REVERTED;
} else if ($this->getPusherIntent() === ReleephRequest::INTENT_PASS) {
// Otherwise, if it has never been in the branch, explicitly say why:
return ReleephRequestStatus::STATUS_REJECTED;
} else if ($this->getRequestorIntent() === ReleephRequest::INTENT_WANT) {
return ReleephRequestStatus::STATUS_REQUESTED;
} else {
return ReleephRequestStatus::STATUS_ABANDONED;
}
}
}
}
/* -( Lisk mechanics )----------------------------------------------------- */
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
'userIntents' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'requestCommitPHID' => 'phid?',
'commitIdentifier' => 'text40?',
'commitPHID' => 'phid?',
'pickStatus' => 'uint32?',
'inBranch' => 'bool',
'mailKey' => 'bytes20',
'userIntents' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'requestIdentifierBranch' => array(
'columns' => array('requestCommitPHID', 'branchID'),
'unique' => true,
),
'branchID' => array(
'columns' => array('branchID'),
),
'key_requestedObject' => array(
'columns' => array('requestedObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
ReleephRequestPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( Helpful accessors )--------------------------------------------------- */
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
/**
* Get the commit PHIDs this request is requesting.
*
* NOTE: For now, this always returns one PHID.
*
* @return list<phid> Commit PHIDs requested by this request.
*/
public function getCommitPHIDs() {
return array(
$this->requestCommitPHID,
);
}
public function getReason() {
// Backward compatibility: reason used to be called comments
$reason = $this->getDetail('reason');
if (!$reason) {
return $this->getDetail('comments');
}
return $reason;
}
/**
* Allow a null summary, and fall back to the title of the commit.
*/
public function getSummaryForDisplay() {
$summary = $this->getDetail('summary');
if (!strlen($summary)) {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
$summary = $commit->getSummary();
}
}
if (!strlen($summary)) {
$summary = pht('None');
}
return $summary;
}
/* -( Loading external objects )------------------------------------------- */
public function loadPhabricatorRepositoryCommit() {
return $this->loadOneRelative(
new PhabricatorRepositoryCommit(),
'phid',
'getRequestCommitPHID');
}
public function loadPhabricatorRepositoryCommitData() {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
return $commit->loadOneRelative(
new PhabricatorRepositoryCommitData(),
'commitID');
}
}
/* -( State change helpers )----------------------------------------------- */
public function setUserIntent(PhabricatorUser $user, $intent) {
$this->userIntents[$user->getPHID()] = $intent;
return $this;
}
/* -( Migrating to status-less ReleephRequests )--------------------------- */
protected function didReadData() {
if ($this->userIntents === null) {
$this->userIntents = array();
}
}
public function setStatus($value) {
throw new Exception('`status` is now deprecated!');
}
/* -( Make magic Lisk methods private )------------------------------------ */
private function setUserIntents(array $ar) {
return parent::setUserIntents($ar);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephRequestTransactionalEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ReleephRequestTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBranch()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBranch()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Pull requests have the same policies as the branches they are '.
'requested against.');
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('releeph.fields');
}
public function getCustomFieldBaseClass() {
return 'ReleephFieldSpecification';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index ceae8cbf2e..bf60744b55 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,1608 +1,1615 @@
<?php
/**
* @task uri Repository URI Management
* @task autoclose Autoclose
*/
final class PhabricatorRepository extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorMarkupInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface {
/**
* Shortest hash we'll recognize in raw "a829f32" form.
*/
const MINIMUM_UNQUALIFIED_HASH = 7;
/**
* Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
*/
const MINIMUM_QUALIFIED_HASH = 5;
const TABLE_PATH = 'repository_path';
const TABLE_PATHCHANGE = 'repository_pathchange';
const TABLE_FILESYSTEM = 'repository_filesystem';
const TABLE_SUMMARY = 'repository_summary';
const TABLE_BADCOMMIT = 'repository_badcommit';
const TABLE_LINTMESSAGE = 'repository_lintmessage';
const TABLE_PARENTS = 'repository_parents';
const TABLE_COVERAGE = 'repository_coverage';
const SERVE_OFF = 'off';
const SERVE_READONLY = 'readonly';
const SERVE_READWRITE = 'readwrite';
const BECAUSE_REPOSITORY_IMPORTING = 'auto/importing';
const BECAUSE_AUTOCLOSE_DISABLED = 'auto/disabled';
const BECAUSE_NOT_ON_AUTOCLOSE_BRANCH = 'auto/nobranch';
const BECAUSE_BRANCH_UNTRACKED = 'auto/notrack';
const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
protected $name;
protected $callsign;
protected $uuid;
protected $viewPolicy;
protected $editPolicy;
protected $pushPolicy;
protected $versionControlSystem;
protected $details = array();
protected $credentialPHID;
private $commitCount = self::ATTACHABLE;
private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE;
public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDiffusionApplication'))
->executeOne();
$view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
$push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
return id(new PhabricatorRepository())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setPushPolicy($push_policy);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'callsign' => 'sort32',
'versionControlSystem' => 'text32',
'uuid' => 'text64?',
'pushPolicy' => 'policy',
'credentialPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'callsign' => array(
'columns' => array('callsign'),
'unique' => true,
),
'key_name' => array(
'columns' => array('name(128)'),
),
'key_vcs' => array(
'columns' => array('versionControlSystem'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
}
public function toDictionary() {
return array(
'id' => $this->getID(),
'name' => $this->getName(),
'phid' => $this->getPHID(),
'callsign' => $this->getCallsign(),
'monogram' => $this->getMonogram(),
'vcs' => $this->getVersionControlSystem(),
'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
'remoteURI' => (string)$this->getRemoteURI(),
'description' => $this->getDetail('description'),
'isActive' => $this->isTracked(),
'isHosted' => $this->isHosted(),
'isImporting' => $this->isImporting(),
);
}
public function getMonogram() {
return 'r'.$this->getCallsign();
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function getHumanReadableDetail($key, $default = null) {
$value = $this->getDetail($key, $default);
switch ($key) {
case 'branch-filter':
case 'close-commits-filter':
$value = array_keys($value);
$value = implode(', ', $value);
break;
}
return $value;
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function attachCommitCount($count) {
$this->commitCount = $count;
return $this;
}
public function getCommitCount() {
return $this->assertAttached($this->commitCount);
}
public function attachMostRecentCommit(
PhabricatorRepositoryCommit $commit = null) {
$this->mostRecentCommit = $commit;
return $this;
}
public function getMostRecentCommit() {
return $this->assertAttached($this->mostRecentCommit);
}
public function getDiffusionBrowseURIForPath(
PhabricatorUser $user,
$path,
$line = null,
$branch = null) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $this,
'path' => $path,
'branch' => $branch,
));
return $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line,
));
}
public function getLocalPath() {
return $this->getDetail('local-path');
}
public function getSubversionBaseURI($commit = null) {
$subpath = $this->getDetail('svn-subpath');
if (!strlen($subpath)) {
$subpath = null;
}
return $this->getSubversionPathURI($subpath, $commit);
}
public function getSubversionPathURI($path = null, $commit = null) {
$vcs = $this->getVersionControlSystem();
if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
throw new Exception('Not a subversion repository!');
}
if ($this->isHosted()) {
$uri = 'file://'.$this->getLocalPath();
} else {
$uri = $this->getDetail('remote-uri');
}
$uri = rtrim($uri, '/');
if (strlen($path)) {
$path = rawurlencode($path);
$path = str_replace('%2F', '/', $path);
$uri = $uri.'/'.ltrim($path, '/');
}
if ($path !== null || $commit !== null) {
$uri .= '@';
}
if ($commit !== null) {
$uri .= $commit;
}
return $uri;
}
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/**
* Get the name of the directory this repository should clone or checkout
* into. For example, if the repository name is "Example Repository", a
* reasonable name might be "example-repository". This is used to help users
* get reasonable results when cloning repositories, since they generally do
* not want to clone into directories called "X/" or "Example Repository/".
*
* @return string
*/
public function getCloneName() {
$name = $this->getDetail('clone-name');
// Make some reasonable effort to produce reasonable default directory
// names from repository names.
if (!strlen($name)) {
$name = $this->getName();
$name = phutil_utf8_strtolower($name);
$name = preg_replace('@[/ -:]+@', '-', $name);
$name = trim($name, '-');
if (!strlen($name)) {
$name = $this->getCallsign();
}
}
return $name;
}
/* -( Remote Command Execution )------------------------------------------- */
public function execRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolve();
}
public function execxRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolvex();
}
public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args);
}
public function passthruRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandPassthru($args)->execute();
}
private function newRemoteCommandFuture(array $argv) {
$argv = $this->formatRemoteCommand($argv);
$future = newv('ExecFuture', $argv);
$future->setEnv($this->getRemoteCommandEnvironment());
return $future;
}
private function newRemoteCommandPassthru(array $argv) {
$argv = $this->formatRemoteCommand($argv);
$passthru = newv('PhutilExecPassthru', $argv);
$passthru->setEnv($this->getRemoteCommandEnvironment());
return $passthru;
}
/* -( Local Command Execution )-------------------------------------------- */
public function execLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolve();
}
public function execxLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolvex();
}
public function getLocalCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args);
}
public function passthruLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandPassthru($args)->execute();
}
private function newLocalCommandFuture(array $argv) {
$this->assertLocalExists();
$argv = $this->formatLocalCommand($argv);
$future = newv('ExecFuture', $argv);
$future->setEnv($this->getLocalCommandEnvironment());
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
private function newLocalCommandPassthru(array $argv) {
$this->assertLocalExists();
$argv = $this->formatLocalCommand($argv);
$future = newv('PhutilExecPassthru', $argv);
$future->setEnv($this->getLocalCommandEnvironment());
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
/* -( Command Infrastructure )--------------------------------------------- */
private function getSSHWrapper() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/bin/ssh-connect';
}
private function getCommonCommandEnvironment() {
$env = array(
// NOTE: Force the language to "en_US.UTF-8", which overrides locale
// settings. This makes stuff print in English instead of, e.g., French,
// so we can parse the output of some commands, error messages, etc.
'LANG' => 'en_US.UTF-8',
// Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155.
'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(),
);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if
// it can not read $HOME. For many users, $HOME points at /root (this
// seems to be a default result of Apache setup). Instead, explicitly
// point $HOME at a readable, empty directory so that Git looks for the
// config file it's after, fails to locate it, and moves on. This is
// really silly, but seems like the least damaging approach to
// mitigating the issue.
$root = dirname(phutil_get_library_root('phabricator'));
$env['HOME'] = $root.'/support/empty/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: This overrides certain configuration, extensions, and settings
// which make Mercurial commands do random unusual things.
$env['HGPLAIN'] = 1;
break;
default:
throw new Exception('Unrecognized version control system.');
}
return $env;
}
private function getLocalCommandEnvironment() {
return $this->getCommonCommandEnvironment();
}
private function getRemoteCommandEnvironment() {
$env = $this->getCommonCommandEnvironment();
if ($this->shouldUseSSH()) {
// NOTE: This is read by `bin/ssh-connect`, and tells it which credentials
// to use.
$env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// Force SVN to use `bin/ssh-connect`.
$env['SVN_SSH'] = $this->getSSHWrapper();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// Force Git to use `bin/ssh-connect`.
$env['GIT_SSH'] = $this->getSSHWrapper();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// We force Mercurial through `bin/ssh-connect` too, but it uses a
// command-line flag instead of an environmental variable.
break;
default:
throw new Exception('Unrecognized version control system.');
}
}
return $env;
}
private function formatRemoteCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) {
$flags = array();
$flag_args = array();
$flags[] = '--non-interactive';
$flags[] = '--no-auth-cache';
if ($this->shouldUseHTTP()) {
$flags[] = '--trust-server-cert';
}
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$flags[] = '--username %P';
$flags[] = '--password %P';
$flag_args[] = $key->getUsernameEnvelope();
$flag_args[] = $key->getPasswordEnvelope();
}
$flags = implode(' ', $flags);
$pattern = "svn {$flags} {$pattern}";
$args = array_mergev(array($flag_args, $args));
} else {
$pattern = "svn --non-interactive {$pattern}";
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($this->shouldUseSSH()) {
$pattern = "hg --config ui.ssh=%s {$pattern}";
array_unshift(
$args,
$this->getSSHWrapper());
} else {
$pattern = "hg {$pattern}";
}
break;
default:
throw new Exception('Unrecognized version control system.');
}
array_unshift($args, $pattern);
return $args;
}
private function formatLocalCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern = "svn --non-interactive {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$pattern = "hg {$pattern}";
break;
default:
throw new Exception('Unrecognized version control system.');
}
array_unshift($args, $pattern);
return $args;
}
/**
* Sanitize output of an `hg` command invoked with the `--debug` flag to make
* it usable.
*
* @param string Output from `hg --debug ...`
* @return string Usable output.
*/
public static function filterMercurialDebugOutput($stdout) {
// When hg commands are run with `--debug` and some config file isn't
// trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011.
//
// http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html
$lines = preg_split('/(?<=\n)/', $stdout);
$regex = '/ignoring untrusted configuration option .*\n$/';
foreach ($lines as $key => $line) {
$lines[$key] = preg_replace($regex, '', $line);
}
return implode('', $lines);
}
public function getURI() {
return '/diffusion/'.$this->getCallsign().'/';
}
public function getNormalizedPath() {
$uri = (string)$this->getCloneURIObject();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$normalized_uri = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_GIT,
$uri);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$normalized_uri = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_SVN,
$uri);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$normalized_uri = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
$uri);
break;
default:
throw new Exception('Unrecognized version control system.');
}
return $normalized_uri->getNormalizedPath();
}
public function isTracked() {
return $this->getDetail('tracking-enabled', false);
}
public function getDefaultBranch() {
$default = $this->getDetail('default-branch');
if (strlen($default)) {
return $default;
}
$default_branches = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
);
return idx($default_branches, $this->getVersionControlSystem());
}
public function getDefaultArcanistBranch() {
return coalesce($this->getDefaultBranch(), 'svn');
}
private function isBranchInFilter($branch, $filter_key) {
$vcs = $this->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$use_filter = ($is_git);
if (!$use_filter) {
// If this VCS doesn't use filters, pass everything through.
return true;
}
$filter = $this->getDetail($filter_key, array());
// If there's no filter set, let everything through.
if (!$filter) {
return true;
}
// If this branch isn't literally named `regexp(...)`, and it's in the
// filter list, let it through.
if (isset($filter[$branch])) {
if (self::extractBranchRegexp($branch) === null) {
return true;
}
}
// If the branch matches a regexp, let it through.
foreach ($filter as $pattern => $ignored) {
$regexp = self::extractBranchRegexp($pattern);
if ($regexp !== null) {
if (preg_match($regexp, $branch)) {
return true;
}
}
}
// Nothing matched, so filter this branch out.
return false;
}
public static function extractBranchRegexp($pattern) {
$matches = null;
if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
return $matches[1];
}
return null;
}
public function shouldTrackBranch($branch) {
return $this->isBranchInFilter($branch, 'branch-filter');
}
public function formatCommitName($commit_identifier) {
$vcs = $this->getVersionControlSystem();
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$is_git = ($vcs == $type_git);
$is_hg = ($vcs == $type_hg);
if ($is_git || $is_hg) {
$short_identifier = substr($commit_identifier, 0, 12);
} else {
$short_identifier = $commit_identifier;
}
return 'r'.$this->getCallsign().$short_identifier;
}
public function isImporting() {
return (bool)$this->getDetail('importing', false);
}
/* -( Autoclose )---------------------------------------------------------- */
/**
* Determine if autoclose is active for a branch.
*
* For more details about why, use @{method:shouldSkipAutocloseBranch}.
*
* @param string Branch name to check.
* @return bool True if autoclose is active for the branch.
* @task autoclose
*/
public function shouldAutocloseBranch($branch) {
return ($this->shouldSkipAutocloseBranch($branch) === null);
}
/**
* Determine if autoclose is active for a commit.
*
* For more details about why, use @{method:shouldSkipAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return bool True if autoclose is active for the commit.
* @task autoclose
*/
public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) {
return ($this->shouldSkipAutocloseCommit($commit) === null);
}
/**
* Determine why autoclose should be skipped for a branch.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseBranch}.
*
* @param string Branch name to check.
* @return const|null Constant identifying reason to skip this branch, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseBranch($branch) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
if (!$this->shouldTrackBranch($branch)) {
return self::BECAUSE_BRANCH_UNTRACKED;
}
if (!$this->isBranchInFilter($branch, 'close-commits-filter')) {
return self::BECAUSE_BRANCH_NOT_AUTOCLOSE;
}
return null;
}
/**
* Determine why autoclose should be skipped for a commit.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return const|null Constant identifying reason to skip this commit, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseCommit(
PhabricatorRepositoryCommit $commit) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return null;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
throw new Exception('Unrecognized version control system.');
}
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
if (!$commit->isPartiallyImported($closeable_flag)) {
return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH;
}
return null;
}
/**
* Determine why all autoclose operations should be skipped for this
* repository.
*
* @return const|null Constant identifying reason to skip all autoclose
* operations, or null if autoclose operations are not blocked at the
* repository level.
* @task autoclose
*/
private function shouldSkipAllAutoclose() {
if ($this->isImporting()) {
return self::BECAUSE_REPOSITORY_IMPORTING;
}
if ($this->getDetail('disable-autoclose', false)) {
return self::BECAUSE_AUTOCLOSE_DISABLED;
}
return null;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public function getRemoteURI() {
return (string)$this->getRemoteURIObject();
}
/**
* Get the remote URI for this repository, including credentials if they're
* used by this repository.
*
* @return PhutilOpaqueEnvelope URI, possibly including credentials.
* @task uri
*/
public function getRemoteURIEnvelope() {
$uri = $this->getRemoteURIObject();
$remote_protocol = $this->getRemoteProtocol();
if ($remote_protocol == 'http' || $remote_protocol == 'https') {
// For SVN, we use `--username` and `--password` flags separately, so
// don't add any credentials here.
if (!$this->isSVN()) {
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$uri->setUser($key->getUsernameEnvelope()->openEnvelope());
$uri->setPass($key->getPasswordEnvelope()->openEnvelope());
}
}
}
return new PhutilOpaqueEnvelope((string)$uri);
}
/**
* Get the clone (or checkout) URI for this repository, without authentication
* information.
*
* @return string Repository URI.
* @task uri
*/
public function getPublicCloneURI() {
$uri = $this->getCloneURIObject();
// Make sure we don't leak anything if this repo is using HTTP Basic Auth
// with the credentials in the URI or something zany like that.
// If repository is not accessed over SSH we remove both username and
// password.
if (!$this->isHosted()) {
if (!$this->shouldUseSSH()) {
$uri->setUser(null);
// This might be a Git URI or a normal URI. If it's Git, there's no
// password support.
if ($uri instanceof PhutilURI) {
$uri->setPass(null);
}
}
}
return (string)$uri;
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public function getRemoteProtocol() {
$uri = $this->getRemoteURIObject();
if ($uri instanceof PhutilGitURI) {
return 'ssh';
} else {
return $uri->getProtocol();
}
}
/**
* Get a parsed object representation of the repository's remote URI. This
* may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git
* URI (returned as a @{class@libphutil:PhutilGitURI}).
*
* @return wild A @{class@libphutil:PhutilURI} or
* @{class@libphutil:PhutilGitURI}.
* @task uri
*/
public function getRemoteURIObject() {
$raw_uri = $this->getDetail('remote-uri');
if (!$raw_uri) {
return new PhutilURI('');
}
if (!strncmp($raw_uri, '/', 1)) {
return new PhutilURI('file://'.$raw_uri);
}
$uri = new PhutilURI($raw_uri);
if ($uri->getProtocol()) {
return $uri;
}
$uri = new PhutilGitURI($raw_uri);
if ($uri->getDomain()) {
return $uri;
}
throw new Exception("Remote URI '{$raw_uri}' could not be parsed!");
}
/**
* Get the "best" clone/checkout URI for this repository, on any protocol.
*/
public function getCloneURIObject() {
if (!$this->isHosted()) {
if ($this->isSVN()) {
// Make sure we pick up the "Import Only" path for Subversion, so
// the user clones the repository starting at the correct path, not
// from the root.
$base_uri = $this->getSubversionBaseURI();
$base_uri = new PhutilURI($base_uri);
$path = $base_uri->getPath();
if (!$path) {
$path = '/';
}
// If the trailing "@" is not required to escape the URI, strip it for
// readability.
if (!preg_match('/@.*@/', $path)) {
$path = rtrim($path, '@');
}
$base_uri->setPath($path);
return $base_uri;
} else {
return $this->getRemoteURIObject();
}
}
// Choose the best URI: pick a read/write URI over a URI which is not
// read/write, and SSH over HTTP.
$serve_ssh = $this->getServeOverSSH();
$serve_http = $this->getServeOverHTTP();
if ($serve_ssh === self::SERVE_READWRITE) {
return $this->getSSHCloneURIObject();
} else if ($serve_http === self::SERVE_READWRITE) {
return $this->getHTTPCloneURIObject();
} else if ($serve_ssh !== self::SERVE_OFF) {
return $this->getSSHCloneURIObject();
} else if ($serve_http !== self::SERVE_OFF) {
return $this->getHTTPCloneURIObject();
} else {
return null;
}
}
/**
* Get the repository's SSH clone/checkout URI, if one exists.
*/
public function getSSHCloneURIObject() {
if (!$this->isHosted()) {
if ($this->shouldUseSSH()) {
return $this->getRemoteURIObject();
} else {
return null;
}
}
$serve_ssh = $this->getServeOverSSH();
if ($serve_ssh === self::SERVE_OFF) {
return null;
}
$uri = new PhutilURI(PhabricatorEnv::getProductionURI($this->getURI()));
if ($this->isSVN()) {
$uri->setProtocol('svn+ssh');
} else {
$uri->setProtocol('ssh');
}
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
$ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
if ($ssh_user) {
$uri->setUser($ssh_user);
}
$uri->setPort(PhabricatorEnv::getEnvConfig('diffusion.ssh-port'));
return $uri;
}
/**
* Get the repository's HTTP clone/checkout URI, if one exists.
*/
public function getHTTPCloneURIObject() {
if (!$this->isHosted()) {
if ($this->shouldUseHTTP()) {
return $this->getRemoteURIObject();
} else {
return null;
}
}
$serve_http = $this->getServeOverHTTP();
if ($serve_http === self::SERVE_OFF) {
return null;
}
$uri = PhabricatorEnv::getProductionURI($this->getURI());
$uri = new PhutilURI($uri);
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
return $uri;
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private function shouldUseSSH() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
if ($this->isSSHProtocol($protocol)) {
return true;
}
return false;
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private function shouldUseHTTP() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'http' || $protocol == 'https');
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private function shouldUseSVNProtocol() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'svn');
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private function isSSHProtocol($protocol) {
return ($protocol == 'ssh' || $protocol == 'svn+ssh');
}
public function delete() {
$this->openTransaction();
$paths = id(new PhabricatorOwnersPath())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($paths as $path) {
$path->delete();
}
$projects = id(new PhabricatorRepositoryArcanistProject())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($projects as $project) {
// note each project deletes its PhabricatorRepositorySymbols
$project->delete();
}
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($commits as $commit) {
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit->delete();
}
$mirrors = id(new PhabricatorRepositoryMirror())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($mirrors as $mirror) {
$mirror->delete();
}
$ref_cursors = id(new PhabricatorRepositoryRefCursor())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($ref_cursors as $cursor) {
$cursor->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_FILESYSTEM,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_PATHCHANGE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_SUMMARY,
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function isGit() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
public function isSVN() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
}
public function isHg() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
}
public function isHosted() {
return (bool)$this->getDetail('hosting-enabled', false);
}
public function setHosted($enabled) {
return $this->setDetail('hosting-enabled', $enabled);
}
public function getServeOverHTTP() {
if ($this->isSVN()) {
return self::SERVE_OFF;
}
$serve = $this->getDetail('serve-over-http', self::SERVE_OFF);
return $this->normalizeServeConfigSetting($serve);
}
public function setServeOverHTTP($mode) {
return $this->setDetail('serve-over-http', $mode);
}
public function getServeOverSSH() {
$serve = $this->getDetail('serve-over-ssh', self::SERVE_OFF);
return $this->normalizeServeConfigSetting($serve);
}
public function setServeOverSSH($mode) {
return $this->setDetail('serve-over-ssh', $mode);
}
public static function getProtocolAvailabilityName($constant) {
switch ($constant) {
case self::SERVE_OFF:
return pht('Off');
case self::SERVE_READONLY:
return pht('Read Only');
case self::SERVE_READWRITE:
return pht('Read/Write');
default:
return pht('Unknown');
}
}
private function normalizeServeConfigSetting($value) {
switch ($value) {
case self::SERVE_OFF:
case self::SERVE_READONLY:
return $value;
case self::SERVE_READWRITE:
if ($this->isHosted()) {
return self::SERVE_READWRITE;
} else {
return self::SERVE_READONLY;
}
default:
return self::SERVE_OFF;
}
}
/**
* Raise more useful errors when there are basic filesystem problems.
*/
private function assertLocalExists() {
if (!$this->usesLocalWorkingCopy()) {
return;
}
$local = $this->getLocalPath();
Filesystem::assertExists($local);
Filesystem::assertIsDirectory($local);
Filesystem::assertReadable($local);
}
/**
* Determine if the working copy is bare or not. In Git, this corresponds
* to `--bare`. In Mercurial, `--noupdate`.
*/
public function isWorkingCopyBare() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return false;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$local = $this->getLocalPath();
if (Filesystem::pathExists($local.'/.git')) {
return false;
} else {
return true;
}
}
}
public function usesLocalWorkingCopy() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->isHosted();
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return true;
}
}
public function getHookDirectories() {
$directories = array();
if (!$this->isHosted()) {
return $directories;
}
$root = $this->getLocalPath();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($this->isWorkingCopyBare()) {
$directories[] = $root.'/hooks/pre-receive-phabricator.d/';
} else {
$directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$directories[] = $root.'/hooks/pre-commit-phabricator.d/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: We don't support custom Mercurial hooks for now because they're
// messy and we can't easily just drop a `hooks.d/` directory next to
// the hooks.
break;
}
return $directories;
}
public function canDestroyWorkingCopy() {
if ($this->isHosted()) {
// Never destroy hosted working copies.
return false;
}
$default_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
}
public function canUsePathTree() {
return !$this->isSVN();
}
public function canMirror() {
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function canAllowDangerousChanges() {
if (!$this->isHosted()) {
return false;
}
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function shouldAllowDangerousChanges() {
return (bool)$this->getDetail('allow-dangerous-changes');
}
public function writeStatusMessage(
$status_type,
$status_code,
array $parameters = array()) {
$table = new PhabricatorRepositoryStatusMessage();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
if ($status_code === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
$table_name,
$this->getID(),
$status_type);
} else {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, statusType, statusCode, parameters, epoch)
VALUES (%d, %s, %s, %s, %d)
ON DUPLICATE KEY UPDATE
statusCode = VALUES(statusCode),
parameters = VALUES(parameters),
epoch = VALUES(epoch)',
$table_name,
$this->getID(),
$status_type,
$status_code,
json_encode($parameters),
time());
}
return $this;
}
public static function getRemoteURIProtocol($raw_uri) {
$uri = new PhutilURI($raw_uri);
if ($uri->getProtocol()) {
return strtolower($uri->getProtocol());
}
$git_uri = new PhutilGitURI($raw_uri);
if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) {
return 'ssh';
}
return null;
}
public static function assertValidRemoteURI($uri) {
if (trim($uri) != $uri) {
throw new Exception(
pht(
'The remote URI has leading or trailing whitespace.'));
}
$protocol = self::getRemoteURIProtocol($uri);
// Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
// for discussion. This is usually a user adding "ssh://" to an implicit
// SSH Git URI.
if ($protocol == 'ssh') {
if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
throw new Exception(
pht(
"The remote URI is not formatted correctly. Remote URIs ".
"with an explicit protocol should be in the form ".
"'proto://domain/path', not 'proto://domain:/path'. ".
"The ':/path' syntax is only valid in SCP-style URIs."));
}
}
switch ($protocol) {
case 'ssh':
case 'http':
case 'https':
case 'git':
case 'svn':
case 'svn+ssh':
break;
default:
// NOTE: We're explicitly rejecting 'file://' because it can be
// used to clone from the working copy of another repository on disk
// that you don't normally have permission to access.
throw new Exception(
pht(
"The URI protocol is unrecognized. It should begin ".
"'ssh://', 'http://', 'https://', 'git://', 'svn://', ".
"'svn+ssh://', or be in the form 'git@domain.com:path'."));
}
return true;
}
/**
* Load the pull frequency for this repository, based on the time since the
* last activity.
*
* We pull rarely used repositories less frequently. This finds the most
* recent commit which is older than the current time (which prevents us from
* spinning on repositories with a silly commit post-dated to some time in
* 2037). We adjust the pull frequency based on when the most recent commit
* occurred.
*
* @param int The minimum update interval to use, in seconds.
* @return int Repository update interval, in seconds.
*/
public function loadUpdateInterval($minimum = 15) {
// If a repository is still importing, always pull it as frequently as
// possible. This prevents us from hanging for a long time at 99.9% when
// importing an inactive repository.
if ($this->isImporting()) {
return $minimum;
}
$window_start = (PhabricatorTime::getNow() + $minimum);
$table = id(new PhabricatorRepositoryCommit());
$last_commit = queryfx_one(
$table->establishConnection('r'),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1',
$table->getTableName(),
$this->getID(),
$window_start);
if ($last_commit) {
$time_since_commit = ($window_start - $last_commit['epoch']);
$last_few_days = phutil_units('3 days in seconds');
if ($time_since_commit <= $last_few_days) {
// For repositories with activity in the recent past, we wait one
// extra second for every 10 minutes since the last commit. This
// shorter backoff is intended to handle weekends and other short
// breaks from development.
$smart_wait = ($time_since_commit / 600);
} else {
// For repositories without recent activity, we wait one extra second
// for every 4 minutes since the last commit. This longer backoff
// handles rarely used repositories, up to the maximum.
$smart_wait = ($time_since_commit / 240);
}
// We'll never wait more than 6 hours to pull a repository.
$longest_wait = phutil_units('6 hours in seconds');
$smart_wait = min($smart_wait, $longest_wait);
$smart_wait = max($minimum, $smart_wait);
} else {
$smart_wait = $minimum;
}
return $smart_wait;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorRepositoryEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
DiffusionPushCapability::CAPABILITY,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case DiffusionPushCapability::CAPABILITY:
return $this->getPushPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
return "repo:{$hash}";
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
return $this->getDetail('description');
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return true;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
index 2a3c4551bb..92d5df0da6 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -1,405 +1,412 @@
<?php
final class PhabricatorRepositoryCommit
extends PhabricatorRepositoryDAO
implements
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorProjectInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
HarbormasterBuildableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface {
protected $repositoryID;
protected $phid;
protected $commitIdentifier;
protected $epoch;
protected $mailKey;
protected $authorPHID;
protected $auditStatus = PhabricatorAuditCommitStatusConstants::NONE;
protected $summary = '';
protected $importStatus = 0;
const IMPORTED_MESSAGE = 1;
const IMPORTED_CHANGE = 2;
const IMPORTED_OWNERS = 4;
const IMPORTED_HERALD = 8;
const IMPORTED_ALL = 15;
const IMPORTED_CLOSEABLE = 1024;
private $commitData = self::ATTACHABLE;
private $audits = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository($assert_attached = true) {
if ($assert_attached) {
return $this->assertAttached($this->repository);
}
return $this->repository;
}
public function isPartiallyImported($mask) {
return (($mask & $this->getImportStatus()) == $mask);
}
public function isImported() {
return $this->isPartiallyImported(self::IMPORTED_ALL);
}
public function writeImportStatusFlag($flag) {
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d',
$this->getTableName(),
$flag,
$this->getID());
$this->setImportStatus($this->getImportStatus() | $flag);
return $this;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'commitIdentifier' => 'text40',
'mailKey' => 'bytes20',
'authorPHID' => 'phid?',
'auditStatus' => 'uint32',
'summary' => 'text80',
'importStatus' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'repositoryID' => array(
'columns' => array('repositoryID', 'importStatus'),
),
'authorPHID' => array(
'columns' => array('authorPHID', 'auditStatus', 'epoch'),
),
'repositoryID_2' => array(
'columns' => array('repositoryID', 'epoch'),
),
'key_commit_identity' => array(
'columns' => array('commitIdentifier', 'repositoryID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryCommitPHIDType::TYPECONST);
}
public function loadCommitData() {
if (!$this->getID()) {
return null;
}
return id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$this->getID());
}
public function attachCommitData(
PhabricatorRepositoryCommitData $data = null) {
$this->commitData = $data;
return $this;
}
public function getCommitData() {
return $this->assertAttached($this->commitData);
}
public function attachAudits(array $audits) {
assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest');
$this->audits = $audits;
return $this;
}
public function getAudits() {
return $this->assertAttached($this->audits);
}
public function getAuthorityAudits(
PhabricatorUser $user,
array $authority_phids) {
$authority = array_fill_keys($authority_phids, true);
$audits = $this->getAudits();
$authority_audits = array();
foreach ($audits as $audit) {
$has_authority = !empty($authority[$audit->getAuditorPHID()]);
if ($has_authority) {
$commit_author = $this->getAuthorPHID();
// You don't have authority over package and project audits on your
// own commits.
$auditor_is_user = ($audit->getAuditorPHID() == $user->getPHID());
$user_is_author = ($commit_author == $user->getPHID());
if ($auditor_is_user || !$user_is_author) {
$authority_audits[$audit->getID()] = $audit;
}
}
}
return $authority_audits;
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function delete() {
$data = $this->loadCommitData();
$audits = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere('commitPHID = %s', $this->getPHID());
$this->openTransaction();
if ($data) {
$data->delete();
}
foreach ($audits as $audit) {
$audit->delete();
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function getDateCreated() {
// This is primarily to make analysis of commits with the Fact engine work.
return $this->getEpoch();
}
public function getURI() {
$repository = $this->getRepository();
$callsign = $repository->getCallsign();
$commit_identifier = $this->getCommitIdentifier();
return '/r'.$callsign.$commit_identifier;
}
/**
* Synchronize a commit's overall audit status with the individual audit
* triggers.
*/
public function updateAuditStatus(array $requests) {
assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest');
$any_concern = false;
$any_accept = false;
$any_need = false;
foreach ($requests as $request) {
switch ($request->getAuditStatus()) {
case PhabricatorAuditStatusConstants::AUDIT_REQUIRED:
$any_need = true;
break;
case PhabricatorAuditStatusConstants::ACCEPTED:
$any_accept = true;
break;
case PhabricatorAuditStatusConstants::CONCERNED:
$any_concern = true;
break;
}
}
if ($any_concern) {
$status = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
} else if ($any_accept) {
if ($any_need) {
$status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED;
} else {
$status = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED;
}
} else if ($any_need) {
$status = PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT;
} else {
$status = PhabricatorAuditCommitStatusConstants::NONE;
}
return $this->setAuditStatus($status);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getRepository()->getPolicy($capability);
case PhabricatorPolicyCapability::CAN_EDIT:
// TODO: (T603) Who should be able to edit a commit? For now, retain
// the existing policy.
return PhabricatorPolicies::POLICY_USER;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getRepository()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Commits inherit the policies of the repository they belong to.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( Stuff for serialization )---------------------------------------------- */
/**
* NOTE: this is not a complete serialization; only the 'protected' fields are
* involved. This is due to ease of (ab)using the Lisk abstraction to get this
* done, as well as complexity of the other fields.
*/
public function toDictionary() {
return array(
'repositoryID' => $this->getRepositoryID(),
'phid' => $this->getPHID(),
'commitIdentifier' => $this->getCommitIdentifier(),
'epoch' => $this->getEpoch(),
'mailKey' => $this->getMailKey(),
'authorPHID' => $this->getAuthorPHID(),
'auditStatus' => $this->getAuditStatus(),
'summary' => $this->getSummary(),
'importStatus' => $this->getImportStatus(),
);
}
public static function newFromDictionary(array $dict) {
return id(new PhabricatorRepositoryCommit())
->loadFromArray($dict);
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildablePHID() {
return $this->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getRepository()->getPHID();
}
public function getBuildVariables() {
$results = array();
$results['buildable.commit'] = $this->getCommitIdentifier();
$repo = $this->getRepository();
$results['repository.callsign'] = $repo->getCallsign();
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
return $results;
}
public function getAvailableBuildVariables() {
return array(
'buildable.commit' => pht('The commit identifier, if applicable.'),
'repository.callsign' =>
pht('The callsign of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
);
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
// TODO: We could make this configurable eventually, but just use the
// defaults for now.
return array();
}
public function getCustomFieldBaseClass() {
return 'PhabricatorCommitCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
// TODO: This should also list auditors, but handling that is a bit messy
// right now because we are not guaranteed to have the data.
return ($phid == $this->getAuthorPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuditEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuditTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
index 352ff42948..5bea424ffa 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
@@ -1,197 +1,204 @@
<?php
final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface {
const RESPONSES_VISIBLE = 0;
const RESPONSES_VOTERS = 1;
const RESPONSES_OWNER = 2;
const METHOD_PLURALITY = 0;
const METHOD_APPROVAL = 1;
protected $question;
protected $description;
protected $authorPHID;
protected $responseVisibility;
protected $shuffle;
protected $method;
protected $viewPolicy;
protected $isClosed = 0;
private $options = self::ATTACHABLE;
private $choices = self::ATTACHABLE;
private $viewerChoices = self::ATTACHABLE;
public static function initializeNewPoll(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorSlowvoteApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorSlowvoteDefaultViewCapability::CAPABILITY);
return id(new PhabricatorSlowvotePoll())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'question' => 'text255',
'responseVisibility' => 'uint32',
'shuffle' => 'uint32',
'method' => 'uint32',
'description' => 'text',
'isClosed' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorSlowvotePollPHIDType::TYPECONST);
}
public function getOptions() {
return $this->assertAttached($this->options);
}
public function attachOptions(array $options) {
assert_instances_of($options, 'PhabricatorSlowvoteOption');
$this->options = $options;
return $this;
}
public function getChoices() {
return $this->assertAttached($this->choices);
}
public function attachChoices(array $choices) {
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
$this->choices = $choices;
return $this;
}
public function getViewerChoices(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->viewerChoices, $viewer->getPHID());
}
public function attachViewerChoices(PhabricatorUser $viewer, array $choices) {
if ($this->viewerChoices === self::ATTACHABLE) {
$this->viewerChoices = array();
}
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
$this->viewerChoices[$viewer->getPHID()] = $choices;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorSlowvoteEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorSlowvoteTransaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->viewPolicy;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht('The author of a poll can always view and edit it.');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getAuthorPHID());
}
/* -( PhabricatorDestructableInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere(
'pollID = %d',
$this->getID());
foreach ($choices as $choice) {
$choice->delete();
}
$options = id(new PhabricatorSlowvoteOption())->loadAllWhere(
'pollID = %d',
$this->getID());
foreach ($options as $option) {
$option->delete();
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php
index 1c6b4a0049..fdccf7b775 100644
--- a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php
+++ b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php
@@ -1,58 +1,74 @@
<?php
/**
* Allow infrastructure to apply transactions to the implementing object.
*
* For example, implementing this interface allows Subscriptions to apply CC
* transactions, and allows Harbormaster to apply build result notifications.
*/
interface PhabricatorApplicationTransactionInterface {
/**
* Return a @{class:PhabricatorApplicationTransactionEditor} which can be
* used to apply transactions to this object.
*
* @return PhabricatorApplicationTransactionEditor Editor for this object.
*/
public function getApplicationTransactionEditor();
/**
* Return the object to apply transactions to. Normally this is the current
* object (that is, `$this`), but in some cases transactions may apply to
* a different object: for example, @{class:DifferentialDiff} applies
* transactions to the associated @{class:DifferentialRevision}.
*
* @return PhabricatorLiskDAO Object to apply transactions to.
*/
public function getApplicationTransactionObject();
/**
* Return a template transaction for this object.
*
* @return PhabricatorApplicationTransaction
*/
public function getApplicationTransactionTemplate();
+ /**
+ * Hook to augment the $timeline with additional data for rendering.
+ *
+ * @return PhabricatorApplicationTransactionView
+ */
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request);
+
}
// TEMPLATE IMPLEMENTATION /////////////////////////////////////////////////////
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
/*
public function getApplicationTransactionEditor() {
return new <<<???>>>Editor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new <<<???>>>Transaction();
}
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
*/
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
index 531a3eba70..ce57de1849 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
@@ -1,475 +1,494 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionView extends AphrontView {
private $transactions;
private $engine;
private $showEditActions = true;
private $isPreview;
private $objectPHID;
private $shouldTerminate = false;
private $quoteTargetID;
private $quoteRef;
private $pager;
+ private $renderData = array();
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function setShowEditActions($show_edit_actions) {
$this->showEditActions = $show_edit_actions;
return $this;
}
public function getShowEditActions() {
return $this->showEditActions;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->engine = $engine;
return $this;
}
public function setTransactions(array $transactions) {
assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions() {
return $this->transactions;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
+ /**
+ * This is additional data that may be necessary to render the next set
+ * of transactions. Objects that implement
+ * PhabricatorApplicationTransactionInterface use this data in
+ * willRenderTimeline.
+ */
+ public function setRenderData(array $data) {
+ $this->renderData = $data;
+ return $this;
+ }
+
+ public function getRenderData() {
+ return $this->renderData;
+ }
+
public function buildEvents($with_hiding = false) {
$user = $this->getUser();
$xactions = $this->transactions;
$xactions = $this->filterHiddenTransactions($xactions);
$xactions = $this->groupRelatedTransactions($xactions);
$groups = $this->groupDisplayTransactions($xactions);
// If the viewer has interacted with this object, we hide things from
// before their most recent interaction by default. This tends to make
// very long threads much more manageable, because you don't have to
// scroll through a lot of history and can focus on just new stuff.
$show_group = null;
if ($with_hiding) {
// Find the most recent comment by the viewer.
$group_keys = array_keys($groups);
$group_keys = array_reverse($group_keys);
// If we would only hide a small number of transactions, don't hide
// anything. Just don't examine the last few keys. Also, we always
// want to show the most recent pieces of activity, so don't examine
// the first few keys either.
$group_keys = array_slice($group_keys, 2, -2);
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
foreach ($group_keys as $group_key) {
$group = $groups[$group_key];
foreach ($group as $xaction) {
if ($xaction->getAuthorPHID() == $user->getPHID() &&
$xaction->getTransactionType() == $type_comment) {
// This is the most recent group where the user commented.
$show_group = $group_key;
break 2;
}
}
}
}
$events = array();
$hide_by_default = ($show_group !== null);
foreach ($groups as $group_key => $group) {
if ($hide_by_default && ($show_group === $group_key)) {
$hide_by_default = false;
}
$group_event = null;
foreach ($group as $xaction) {
$event = $this->renderEvent($xaction, $group);
$event->setHideByDefault($hide_by_default);
if (!$group_event) {
$group_event = $event;
} else {
$group_event->addEventToGroup($event);
}
if ($hide_by_default) {
$pager = $this->getPager();
if ($pager) {
$pager->setNextPageID($xaction->getID());
}
}
}
$events[] = $group_event;
}
return $events;
}
public function render() {
if (!$this->getObjectPHID()) {
throw new Exception('Call setObjectPHID() before render()!');
}
$view = $this->buildPHUITimelineView();
if ($this->getShowEditActions()) {
Javelin::initBehavior('phabricator-transaction-list');
}
return $view->render();
}
public function buildPHUITimelineView($with_hiding = true) {
if (!$this->getObjectPHID()) {
throw new Exception(
'Call setObjectPHID() before buildPHUITimelineView()!');
}
$view = new PHUITimelineView();
$view->setShouldTerminate($this->shouldTerminate);
$events = $this->buildEvents($with_hiding);
foreach ($events as $event) {
$view->addEvent($event);
}
if ($this->getPager()) {
$view->setPager($this->getPager());
}
+ if ($this->getRenderData()) {
+ $view->setRenderData($this->getRenderData());
+ }
return $view;
}
protected function getOrBuildEngine() {
if (!$this->engine) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = id(new PhabricatorMarkupEngine())
->setViewer($this->getUser());
foreach ($this->transactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$engine->addObject($xaction->getComment(), $field);
}
$engine->process();
$this->engine = $engine;
}
return $this->engine;
}
private function buildChangeDetailsLink(
PhabricatorApplicationTransaction $xaction) {
return javelin_tag(
'a',
array(
'href' => '/transactions/detail/'.$xaction->getPHID().'/',
'sigil' => 'workflow',
),
pht('(Show Details)'));
}
private function buildExtraInformationLink(
PhabricatorApplicationTransaction $xaction) {
$link = $xaction->renderExtraInformationLink();
if (!$link) {
return null;
}
return phutil_tag(
'span',
array(
'class' => 'phui-timeline-extra-information',
),
array(" \xC2\xB7 ", $link));
}
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
return false;
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = $this->getOrBuildEngine();
$comment = $xaction->getComment();
if ($comment) {
if ($comment->getIsRemoved()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht(
'This comment was removed by %s.',
$xaction->getHandle($comment->getAuthorPHID())->renderLink()));
} else if ($comment->getIsDeleted()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht('This comment has been deleted.'));
} else if ($xaction->hasComment()) {
return javelin_tag(
'span',
array(
'class' => 'transaction-comment',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
$engine->getOutput($comment, $field));
} else {
// This is an empty, non-deleted comment. Usually this happens when
// rendering previews.
return null;
}
}
return null;
}
private function filterHiddenTransactions(array $xactions) {
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHide()) {
unset($xactions[$key]);
}
}
return $xactions;
}
private function groupRelatedTransactions(array $xactions) {
$last = null;
$last_key = null;
$groups = array();
foreach ($xactions as $key => $xaction) {
if ($last && $this->shouldGroupTransactions($last, $xaction)) {
$groups[$last_key][] = $xaction;
unset($xactions[$key]);
} else {
$last = $xaction;
$last_key = $key;
}
}
foreach ($xactions as $key => $xaction) {
$xaction->attachTransactionGroup(idx($groups, $key, array()));
}
return $xactions;
}
private function groupDisplayTransactions(array $xactions) {
$groups = array();
$group = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldDisplayGroupWith($group)) {
$group[] = $xaction;
} else {
if ($group) {
$groups[] = $group;
}
$group = array($xaction);
}
}
if ($group) {
$groups[] = $group;
}
foreach ($groups as $key => $group) {
$group = msort($group, 'getActionStrength');
$group = array_reverse($group);
$groups[$key] = $group;
}
return $groups;
}
private function renderEvent(
PhabricatorApplicationTransaction $xaction,
array $group) {
$viewer = $this->getUser();
$event = id(new PHUITimelineEventView())
->setUser($viewer)
->setTransactionPHID($xaction->getPHID())
->setUserHandle($xaction->getHandle($xaction->getAuthorPHID()))
->setIcon($xaction->getIcon())
->setColor($xaction->getColor());
list($token, $token_removed) = $xaction->getToken();
if ($token) {
$event->setToken($token, $token_removed);
}
if (!$this->shouldSuppressTitle($xaction, $group)) {
$title = $xaction->getTitle();
if ($xaction->hasChangeDetails()) {
if (!$this->isPreview) {
$details = $this->buildChangeDetailsLink($xaction);
$title = array(
$title,
' ',
$details,
);
}
}
if (!$this->isPreview) {
$more = $this->buildExtraInformationLink($xaction);
if ($more) {
$title = array($title, ' ', $more);
}
}
$event->setTitle($title);
}
if ($this->isPreview) {
$event->setIsPreview(true);
} else {
$event
->setDateCreated($xaction->getDateCreated())
->setContentSource($xaction->getContentSource())
->setAnchor($xaction->getID());
}
$transaction_type = $xaction->getTransactionType();
$comment_type = PhabricatorTransactions::TYPE_COMMENT;
$is_normal_comment = ($transaction_type == $comment_type);
if ($this->getShowEditActions() &&
!$this->isPreview &&
$is_normal_comment) {
$has_deleted_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsDeleted();
$has_removed_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsRemoved();
if ($xaction->getCommentVersion() > 1 && !$has_removed_comment) {
$event->setIsEdited(true);
}
// If we have a place for quoted text to go and this is a quotable
// comment, pass the quote target ID to the event view.
if ($this->getQuoteTargetID()) {
if ($xaction->hasComment()) {
if (!$has_removed_comment && !$has_deleted_comment) {
$event->setQuoteTargetID($this->getQuoteTargetID());
$event->setQuoteRef($this->getQuoteRef());
}
}
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($xaction->hasComment() || $has_deleted_comment) {
$has_edit_capability = PhabricatorPolicyFilter::hasCapability(
$viewer,
$xaction,
$can_edit);
if ($has_edit_capability && !$has_removed_comment) {
$event->setIsEditable(true);
}
if ($has_edit_capability || $viewer->getIsAdmin()) {
if (!$has_removed_comment) {
$event->setIsRemovable(true);
}
}
}
}
$comment = $this->renderTransactionContent($xaction);
if ($comment) {
$event->appendChild($comment);
}
return $event;
}
private function shouldSuppressTitle(
PhabricatorApplicationTransaction $xaction,
array $group) {
// This is a little hard-coded, but we don't have any other reasonable
// cases for now. Suppress "commented on" if there are other actions in
// the display group.
if (count($group) > 1) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
return true;
}
}
return false;
}
}
diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php
index df8007de8c..376f747353 100644
--- a/src/view/phui/PHUITimelineView.php
+++ b/src/view/phui/PHUITimelineView.php
@@ -1,191 +1,198 @@
<?php
final class PHUITimelineView extends AphrontView {
private $events = array();
private $id;
private $shouldTerminate = false;
private $shouldAddSpacers = true;
private $pager;
+ private $renderData = array();
public function setID($id) {
$this->id = $id;
return $this;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setShouldAddSpacers($bool) {
$this->shouldAddSpacers = $bool;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
public function addEvent(PHUITimelineEventView $event) {
$this->events[] = $event;
return $this;
}
+ public function setRenderData(array $data) {
+ $this->renderData = $data;
+ return $this;
+ }
+
public function render() {
if ($this->getPager()) {
if ($this->id === null) {
$this->id = celerity_generate_unique_node_id();
}
Javelin::initBehavior(
'phabricator-show-older-transactions',
array(
'timelineID' => $this->id,
+ 'renderData' => $this->renderData,
));
}
$events = $this->buildEvents();
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-view',
'id' => $this->id,
),
$events);
}
public function buildEvents() {
require_celerity_resource('phui-timeline-view-css');
$spacer = self::renderSpacer();
$hide = array();
$show = array();
foreach ($this->events as $event) {
if ($event->getHideByDefault()) {
$hide[] = $event;
} else {
$show[] = $event;
}
}
$events = array();
if ($hide) {
if ($this->getPager()) {
$events[] = javelin_tag(
'div',
array(
'sigil' => 'show-older-block',
'class' => 'phui-timeline-older-transactions-are-hidden',
),
array(
pht('Older changes are hidden. '),
' ',
javelin_tag(
'a',
array(
'href' => (string) $this->getPager()->getNextPageURI(),
'mustcapture' => true,
'sigil' => 'show-older-link',
),
pht('Show older changes.')),
));
} else {
$hidden = phutil_implode_html($spacer, $hide);
$count = count($hide);
$show_id = celerity_generate_unique_node_id();
$hide_id = celerity_generate_unique_node_id();
$link_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'phabricator-show-all-transactions',
array(
'anchors' => array_filter(mpull($hide, 'getAnchor')),
'linkID' => $link_id,
'hideID' => $hide_id,
'showID' => $show_id,
));
$events[] = phutil_tag(
'div',
array(
'id' => $hide_id,
'class' => 'phui-timeline-older-transactions-are-hidden',
),
array(
pht('%s older changes(s) are hidden.', new PhutilNumber($count)),
' ',
javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'id' => $link_id,
),
pht('Show all changes.')),
));
$events[] = phutil_tag(
'div',
array(
'id' => $show_id,
'style' => 'display: none',
),
$hidden);
}
}
if ($hide && $show) {
$events[] = $spacer;
}
if ($show) {
$events[] = phutil_implode_html($spacer, $show);
}
if ($events) {
if ($this->shouldAddSpacers) {
$events = array($spacer, $events, $spacer);
}
} else {
$events = array($spacer);
}
if ($this->shouldTerminate) {
$events[] = self::renderEnder(true);
}
return $events;
}
public static function renderSpacer() {
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'phui-timeline-spacer',
),
'');
}
public static function renderEnder() {
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'the-worlds-end',
),
'');
}
}
diff --git a/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js b/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js
index 112eac1612..294d84337d 100644
--- a/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js
+++ b/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js
@@ -1,104 +1,104 @@
/**
* @provides javelin-behavior-phabricator-show-older-transactions
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
* phabricator-busy
*/
JX.behavior('phabricator-show-older-transactions', function(config) {
var loading = false;
function get_hash() {
return window.location.hash.replace(/^#/, '');
}
function hash_is_hidden() {
var hash = get_hash();
if (!hash) {
return false;
}
var id = 'anchor-'+hash;
try {
JX.$(id);
} catch (not_found_exception) {
return true;
}
return false;
}
function check_hash() {
if (hash_is_hidden()) {
var showOlderBlock = null;
try {
showOlderBlock = JX.DOM.find(
JX.$(config.timelineID),
'div',
'show-older-block');
} catch (not_found_exception) {
// probably a garbage hash and we loaded everything looking
// for it; just abort
if (loading) {
loading = false;
JX.Busy.done();
}
return;
}
var showOlderLink = JX.DOM.find(
showOlderBlock,
'a',
'show-older-link');
if (!loading) {
loading = true;
JX.Busy.start();
}
fetch_older_workflow(
showOlderLink.href,
load_hidden_hash,
showOlderBlock)
.start();
} else {
if (loading) {
loading = false;
JX.Busy.done();
}
try {
var target = JX.$(get_hash());
JX.DOM.scrollTo(target);
} catch (ignored) {
// We did our best.
}
}
}
var show_older = function(swap, r) {
JX.DOM.replace(swap, JX.$H(r.timeline).getFragment());
};
var load_hidden_hash = function(swap, r) {
show_older(swap, r);
check_hash();
};
var fetch_older_workflow = function(href, callback, swap) {
- return new JX.Workflow(href)
+ return new JX.Workflow(href, config.renderData)
.setHandler(JX.bind(null, callback, swap));
};
JX.Stratcom.listen(
'click',
['show-older-link'],
function(e) {
e.kill();
fetch_older_workflow(
e.getNode('tag:a').href,
show_older,
e.getNode('show-older-block'))
.start();
});
JX.Stratcom.listen('hashchange', null, check_hash);
check_hash();
});

File Metadata

Mime Type
text/x-diff
Expires
Sun, Sep 7, 7:19 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
222523
Default Alt Text
(495 KB)

Event Timeline