Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F116976572
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Chunks
Created
Tue, Jun 10, 16:24
Size
4 MB
Mime Type
application/octet-stream
Expires
Thu, Jun 12, 16:24 (1 d, 23 h)
Engine
chunks
Format
Chunks
Handle
VKcvNEhLWxRp
Attached To
rPH Phabricator
Total Chunks
2
Completed Chunks
2
Offset
End
Complete
0
4194304
Yes
4194304
4467717
Yes
View Options
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 d1c9bc705..720683c37 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2468 +1,2369 @@
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* @generated
*/
return array(
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
- 'core.pkg.css' => '3c914d86',
- 'core.pkg.js' => 'e1f0f7bd',
+ 'core.pkg.css' => '2574c199',
+ 'core.pkg.js' => 'b5a949ca',
'differential.pkg.css' => '06dc617c',
- 'differential.pkg.js' => 'c2ca903a',
+ 'differential.pkg.js' => 'c1cfa143',
'diffusion.pkg.css' => 'a2d17c7d',
'diffusion.pkg.js' => '6134c5a1',
'maniphest.pkg.css' => '4845691a',
'maniphest.pkg.js' => '4d7e79c8',
'rsrc/audio/basic/alert.mp3' => '98461568',
'rsrc/audio/basic/bing.mp3' => 'ab8603a5',
'rsrc/audio/basic/pock.mp3' => '0cc772f5',
'rsrc/audio/basic/tap.mp3' => 'fc2fd796',
'rsrc/audio/basic/ting.mp3' => '17660001',
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
'rsrc/css/aphront/dark-console.css' => '0e14e8f6',
'rsrc/css/aphront/dialog-view.css' => '6bfc244b',
'rsrc/css/aphront/list-filter-view.css' => '5d6f0526',
'rsrc/css/aphront/multi-column.css' => '84cc6640',
'rsrc/css/aphront/notification.css' => '457861ec',
'rsrc/css/aphront/panel-view.css' => '8427b78d',
'rsrc/css/aphront/phabricator-nav-view.css' => '694d7723',
'rsrc/css/aphront/table-view.css' => '8c9bbafe',
'rsrc/css/aphront/tokenizer.css' => '15d5ff71',
'rsrc/css/aphront/tooltip.css' => '173b9431',
'rsrc/css/aphront/typeahead-browse.css' => 'f2818435',
'rsrc/css/aphront/typeahead.css' => 'a4a21016',
'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
'rsrc/css/application/auth/auth.css' => '0877ed6e',
'rsrc/css/application/base/main-menu-view.css' => '1802a242',
- 'rsrc/css/application/base/notification-menu.css' => '10685bd4',
+ 'rsrc/css/application/base/notification-menu.css' => 'ef480927',
'rsrc/css/application/base/phui-theme.css' => '9f261c6b',
'rsrc/css/application/base/standard-page-view.css' => '34ee718b',
'rsrc/css/application/chatlog/chatlog.css' => 'd295b020',
'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
'rsrc/css/application/config/config-options.css' => '4615667b',
'rsrc/css/application/config/config-template.css' => '8f18fa41',
'rsrc/css/application/config/setup-issue.css' => '30ee0173',
'rsrc/css/application/config/unhandled-exception.css' => '4c96257a',
'rsrc/css/application/conpherence/color.css' => 'abb4c358',
'rsrc/css/application/conpherence/durable-column.css' => '89ea6bef',
'rsrc/css/application/conpherence/header-pane.css' => 'cb6f4e19',
'rsrc/css/application/conpherence/menu.css' => '69368e97',
'rsrc/css/application/conpherence/message-pane.css' => 'b0f55ecc',
'rsrc/css/application/conpherence/notification.css' => 'cef0a3fc',
'rsrc/css/application/conpherence/participant-pane.css' => '26a3ce56',
'rsrc/css/application/conpherence/transaction.css' => '85129c68',
'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
'rsrc/css/application/countdown/timer.css' => '16c52f5c',
'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a',
'rsrc/css/application/dashboard/dashboard.css' => 'fe5b1869',
'rsrc/css/application/diff/inline-comment-summary.css' => 'f23d4e8f',
'rsrc/css/application/differential/add-comment.css' => 'c47f8c40',
'rsrc/css/application/differential/changeset-view.css' => 'db34a142',
'rsrc/css/application/differential/core.css' => '5b7b8ff4',
'rsrc/css/application/differential/phui-inline-comment.css' => '65ae3bc2',
'rsrc/css/application/differential/revision-comment.css' => '14b8565a',
'rsrc/css/application/differential/revision-history.css' => '0e8eb855',
'rsrc/css/application/differential/revision-list.css' => 'f3c47d33',
'rsrc/css/application/differential/table-of-contents.css' => 'ae4b7a55',
'rsrc/css/application/diffusion/diffusion-icons.css' => '0c15255e',
'rsrc/css/application/diffusion/diffusion-readme.css' => '419dd5b6',
'rsrc/css/application/diffusion/diffusion-repository.css' => 'ee6f20ec',
'rsrc/css/application/diffusion/diffusion.css' => '45727264',
'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948',
'rsrc/css/application/flag/flag.css' => 'bba8f811',
- 'rsrc/css/application/harbormaster/harbormaster.css' => '730a4a3c',
+ 'rsrc/css/application/harbormaster/harbormaster.css' => '7446ce72',
'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
'rsrc/css/application/herald/herald.css' => 'cd8d0134',
'rsrc/css/application/maniphest/report.css' => '9b9580b7',
'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b',
'rsrc/css/application/maniphest/task-summary.css' => '11cc5344',
'rsrc/css/application/objectselector/object-selector.css' => '85ee8ce6',
'rsrc/css/application/owners/owners-path-editor.css' => '9c136c29',
'rsrc/css/application/paste/paste.css' => '9fcc9773',
'rsrc/css/application/people/people-picture-menu-item.css' => 'a06f7f34',
'rsrc/css/application/people/people-profile.css' => '4df76faf',
'rsrc/css/application/phame/phame.css' => '8cb3afcd',
'rsrc/css/application/pholio/pholio-edit.css' => '07676f51',
'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
'rsrc/css/application/pholio/pholio.css' => 'ca89d380',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
'rsrc/css/application/phortune/phortune-invoice.css' => '476055e2',
'rsrc/css/application/phortune/phortune.css' => '5b99dae0',
'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
'rsrc/css/application/phriction/phriction-document-css.css' => '4282e4ad',
'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/ponder-view.css' => 'fbd45f96',
'rsrc/css/application/project/project-card-view.css' => '0010bb52',
'rsrc/css/application/project/project-view.css' => '792c9057',
'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/application-search-view.css' => '787f5b76',
'rsrc/css/application/search/search-results.css' => '505dd8cf',
'rsrc/css/application/slowvote/slowvote.css' => 'a94b7230',
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => '62fa3ace',
- 'rsrc/css/core/remarkup.css' => '7f9ea8b0',
+ 'rsrc/css/core/remarkup.css' => 'b182076e',
'rsrc/css/core/syntax.css' => 'e9c95dd4',
'rsrc/css/core/z-index.css' => '9d8f7c4b',
'rsrc/css/diviner/diviner-shared.css' => '896f1d43',
'rsrc/css/font/font-awesome.css' => 'e838e088',
'rsrc/css/font/font-lato.css' => 'c7ccd872',
'rsrc/css/font/phui-font-icon-base.css' => '870a7360',
- 'rsrc/css/katex.min.css' => '297123ca',
'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97',
- 'rsrc/css/layout/phabricator-source-code-view.css' => '09368218',
+ 'rsrc/css/layout/phabricator-source-code-view.css' => '2ab25dfa',
'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494',
'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68',
- 'rsrc/css/phui/button/phui-button.css' => '1863cc6e',
+ 'rsrc/css/phui/button/phui-button.css' => '6ccb303c',
'rsrc/css/phui/calendar/phui-calendar-day.css' => '572b1893',
'rsrc/css/phui/calendar/phui-calendar-list.css' => '576be600',
'rsrc/css/phui/calendar/phui-calendar-month.css' => '21154caf',
'rsrc/css/phui/calendar/phui-calendar.css' => 'f1ddf11c',
'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '628f59de',
'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77',
'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3',
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6',
- 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'ae1404ba',
+ 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '7c5c1291',
'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea',
'rsrc/css/phui/phui-action-list.css' => '0bcd9a45',
'rsrc/css/phui/phui-action-panel.css' => 'b4798122',
'rsrc/css/phui/phui-badge.css' => '22c0cf4f',
'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3',
'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c',
'rsrc/css/phui/phui-box.css' => '4bd6cdb9',
'rsrc/css/phui/phui-bulk-editor.css' => '9a81e5d5',
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
'rsrc/css/phui/phui-cms.css' => '504b4b23',
'rsrc/css/phui/phui-comment-form.css' => 'ac68149f',
'rsrc/css/phui/phui-comment-panel.css' => 'f50152ad',
- 'rsrc/css/phui/phui-crumbs-view.css' => '6ece3bbb',
+ 'rsrc/css/phui/phui-crumbs-view.css' => '10728aaa',
'rsrc/css/phui/phui-curtain-view.css' => '2bdaf026',
- 'rsrc/css/phui/phui-document-pro.css' => '8af7ea27',
+ 'rsrc/css/phui/phui-document-pro.css' => 'd033e8d5',
'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf',
- 'rsrc/css/phui/phui-document.css' => '391e1e84',
+ 'rsrc/css/phui/phui-document.css' => 'c4ac41f9',
'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9',
'rsrc/css/phui/phui-fontkit.css' => '1320ed01',
- 'rsrc/css/phui/phui-form-view.css' => 'ae9f8d16',
+ 'rsrc/css/phui/phui-form-view.css' => '2f43fae7',
'rsrc/css/phui/phui-form.css' => '7aaa04e3',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
- 'rsrc/css/phui/phui-header-view.css' => '31dc6c72',
+ 'rsrc/css/phui/phui-header-view.css' => '1ba8b707',
'rsrc/css/phui/phui-hovercard.css' => 'f0592bcf',
'rsrc/css/phui/phui-icon-set-selector.css' => '87db8fee',
'rsrc/css/phui/phui-icon.css' => 'cf24ceec',
'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c',
'rsrc/css/phui/phui-info-view.css' => 'e929f98c',
'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0',
'rsrc/css/phui/phui-left-right.css' => '75227a4d',
'rsrc/css/phui/phui-lightbox.css' => '0a035e40',
'rsrc/css/phui/phui-list.css' => '38f8c9bd',
'rsrc/css/phui/phui-object-box.css' => '9cff003c',
'rsrc/css/phui/phui-pager.css' => 'edcbc226',
'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
- 'rsrc/css/phui/phui-property-list-view.css' => 'de4754d8',
+ 'rsrc/css/phui/phui-property-list-view.css' => '546a04ae',
'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863',
'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892',
'rsrc/css/phui/phui-spacing.css' => '042804d6',
'rsrc/css/phui/phui-status.css' => 'd5263e49',
'rsrc/css/phui/phui-tag-view.css' => 'b4719c50',
'rsrc/css/phui/phui-timeline-view.css' => '6ddf8126',
'rsrc/css/phui/phui-two-column-view.css' => '44ec4951',
'rsrc/css/phui/workboards/phui-workboard-color.css' => '783cdff5',
'rsrc/css/phui/workboards/phui-workboard.css' => '3bc85455',
'rsrc/css/phui/workboards/phui-workcard.css' => 'cca5fa92',
'rsrc/css/phui/workboards/phui-workpanel.css' => 'a3a63478',
'rsrc/css/sprite-login.css' => '396f3c3a',
'rsrc/css/sprite-tokens.css' => '9cdfd599',
'rsrc/css/syntax/syntax-default.css' => '9923583c',
'rsrc/externals/d3/d3.min.js' => 'a11a5ff2',
'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '24a7064f',
'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '0039fe26',
'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'de978a43',
'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '2a832057',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.eot' => 'a072da31',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.ttf' => '9b7d7c57',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.woff' => '68bf842e',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.woff2' => '1726711a',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.eot' => '5d91cbdb',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.ttf' => '03559cd7',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.woff' => 'cb0acbc6',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.woff2' => 'a98ed9ec',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.eot' => '40c2c8a3',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.ttf' => 'a48d62bc',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.woff' => 'be0d08c8',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.woff2' => 'bdc3c550',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.eot' => '2729cba3',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.ttf' => '8116dfda',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.woff' => '46415366',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.woff2' => '26b00736',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.eot' => '5a494f47',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.ttf' => 'fc73f831',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.woff' => 'c15bd024',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.woff2' => 'daf53ac5',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.eot' => '0022698c',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.ttf' => '096cbaff',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.woff' => '531dbee4',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.woff2' => '5b6df003',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.eot' => '9d90bdcf',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.ttf' => 'a56597b9',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.woff' => '5b5baa86',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.woff2' => '0285a44c',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.eot' => '91d757f2',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.ttf' => '4d5728b0',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.woff' => '4547be88',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.woff2' => '47d552f7',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.eot' => '7b1aa71e',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.ttf' => '6170bdba',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.woff' => '4bc7d0b0',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.woff2' => '2e87e47c',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.eot' => '7c4b77f8',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.ttf' => '7486170d',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.woff' => '19751090',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.woff2' => 'f5f1ab2d',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.eot' => 'b0da8961',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.ttf' => '846a9d9b',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.woff' => '9c57b47d',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.woff2' => 'ac338b97',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.eot' => '2dcf1c55',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.ttf' => '0711f827',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.woff' => '844ead36',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.woff2' => '76d32fb1',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.eot' => '741eb796',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.ttf' => '632cf898',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.woff' => 'f3bc7fd8',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.woff2' => '3526241c',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.eot' => '0577c8eb',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.ttf' => 'b1c7474c',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.woff' => '18381725',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.woff2' => 'f66cabf5',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.eot' => '72a9ab41',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.ttf' => '7a1f0af6',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.woff' => '9cea9f6b',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.woff2' => '77a8bf3d',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.eot' => 'ba072f12',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.ttf' => '7902fc1d',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.woff' => '97d7f188',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.woff2' => '8b119e4c',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.eot' => 'ca4ff245',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.ttf' => '0c3d257a',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.woff' => '802221cb',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.woff2' => '65aed7c3',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.eot' => '82fa9ba9',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.ttf' => '75619111',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.woff' => 'd4dd5063',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.woff2' => 'f7cc13ed',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.eot' => '3638e7a5',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.ttf' => '6e7a06d3',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.woff' => '25250cb8',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.woff2' => 'f3e00be6',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.eot' => '589b02a7',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.ttf' => 'c61ce183',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.woff' => '7b2cd934',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.woff2' => 'ccbb0a99',
'rsrc/externals/font/lato/lato-bold.eot' => '99fbcf8c',
'rsrc/externals/font/lato/lato-bold.svg' => '2aa83045',
'rsrc/externals/font/lato/lato-bold.ttf' => '0a7141f7',
'rsrc/externals/font/lato/lato-bold.woff' => 'f5db2061',
'rsrc/externals/font/lato/lato-bold.woff2' => '37a94ecd',
'rsrc/externals/font/lato/lato-bolditalic.eot' => 'b93389d0',
'rsrc/externals/font/lato/lato-bolditalic.svg' => '5442e1ef',
'rsrc/externals/font/lato/lato-bolditalic.ttf' => 'dad31252',
'rsrc/externals/font/lato/lato-bolditalic.woff' => 'e53bcf47',
'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'd035007f',
'rsrc/externals/font/lato/lato-italic.eot' => '6a903f5d',
'rsrc/externals/font/lato/lato-italic.svg' => '0dc7cf2f',
'rsrc/externals/font/lato/lato-italic.ttf' => '629f64f0',
'rsrc/externals/font/lato/lato-italic.woff' => '678dc4bb',
'rsrc/externals/font/lato/lato-italic.woff2' => '7c8dd650',
'rsrc/externals/font/lato/lato-regular.eot' => '848dfb1e',
'rsrc/externals/font/lato/lato-regular.svg' => 'cbd5fd6b',
'rsrc/externals/font/lato/lato-regular.ttf' => 'e270165b',
'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2',
'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742',
'rsrc/externals/javelin/core/Event.js' => '2ee659ce',
'rsrc/externals/javelin/core/Stratcom.js' => '327f418a',
'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4',
'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85',
'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313',
'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d',
'rsrc/externals/javelin/core/init.js' => '638a4e2b',
'rsrc/externals/javelin/core/init_node.js' => 'c234aded',
'rsrc/externals/javelin/core/install.js' => '05270951',
'rsrc/externals/javelin/core/util.js' => '93cc50d6',
'rsrc/externals/javelin/docs/Base.js' => '74676256',
'rsrc/externals/javelin/docs/onload.js' => 'e819c479',
'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a',
'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba',
'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212',
'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '2b8de964',
'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '1ad0a787',
'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed',
'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'c90a04fc',
'rsrc/externals/javelin/ext/view/HTMLView.js' => 'fe287620',
'rsrc/externals/javelin/ext/view/View.js' => '0f764c35',
'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'f829edb3',
'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '47830651',
'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2',
'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472',
'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb',
'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b',
'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5',
'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9',
'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03',
'rsrc/externals/javelin/lib/DOM.js' => '4976858c',
'rsrc/externals/javelin/lib/History.js' => 'd4505101',
'rsrc/externals/javelin/lib/JSON.js' => '69adf288',
'rsrc/externals/javelin/lib/Leader.js' => '7f243deb',
'rsrc/externals/javelin/lib/Mask.js' => '8a41885b',
'rsrc/externals/javelin/lib/Quicksand.js' => '6b8ef10b',
'rsrc/externals/javelin/lib/Request.js' => '94b750d2',
'rsrc/externals/javelin/lib/Resource.js' => '44959b73',
'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692',
'rsrc/externals/javelin/lib/Router.js' => '29274e2b',
'rsrc/externals/javelin/lib/Scrollbar.js' => '9065f639',
'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5',
'rsrc/externals/javelin/lib/URI.js' => 'c989ade3',
'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8',
'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6',
'rsrc/externals/javelin/lib/Workflow.js' => '6a726c55',
'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68',
'rsrc/externals/javelin/lib/__tests__/URI.js' => '1e45fda9',
'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783',
'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a',
- 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '8d3bc1b2',
+ 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'bb6e5c16',
'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '185bbd53',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '013ffff9',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa',
'rsrc/favicons/favicon-16x16.png' => 'fc6275ba',
'rsrc/favicons/mask-icon.svg' => 'e132a80f',
'rsrc/image/BFCFDA.png' => 'd5ec91f4',
'rsrc/image/actions/edit.png' => '2fc41442',
'rsrc/image/avatar.png' => '17d346a4',
'rsrc/image/checker_dark.png' => 'd8e65881',
'rsrc/image/checker_light.png' => 'a0155918',
'rsrc/image/checker_lighter.png' => 'd5da91b6',
'rsrc/image/controls/checkbox-checked.png' => 'ad6441ea',
'rsrc/image/controls/checkbox-unchecked.png' => '8eb1f0ae',
'rsrc/image/d5d8e1.png' => '0c2a1497',
'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/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/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_put.png' => '08c95a0c',
'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/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/logo/light-eye.png' => '1a576ddd',
'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/user0.png' => '03dacaea',
'rsrc/image/people/user1.png' => '4a4e7702',
'rsrc/image/people/user2.png' => '47a0ee40',
'rsrc/image/people/user3.png' => '835ff627',
'rsrc/image/people/user4.png' => 'b0e830f1',
'rsrc/image/people/user5.png' => '9c95b369',
'rsrc/image/people/user6.png' => 'ba3fbfb0',
'rsrc/image/people/user7.png' => 'da613924',
'rsrc/image/people/user8.png' => 'f1035edf',
'rsrc/image/people/user9.png' => '66730be3',
'rsrc/image/people/washington.png' => '40dd301c',
'rsrc/image/phrequent_active.png' => 'a466a8ed',
'rsrc/image/phrequent_inactive.png' => 'bfc15a69',
'rsrc/image/resize.png' => 'fd476de4',
'rsrc/image/sprite-login-X2.png' => '308c92c4',
'rsrc/image/sprite-login.png' => '9ec54245',
'rsrc/image/sprite-tokens-X2.png' => '804a5232',
'rsrc/image/sprite-tokens.png' => 'b41d03da',
'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' => 'e1d4b11a',
'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2',
'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '599a8f5f',
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9',
'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289',
'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443',
'rsrc/js/application/calendar/behavior-event-all-day.js' => 'b41537c9',
'rsrc/js/application/calendar/behavior-month-view.js' => 'fe33e256',
'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408',
'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '4d863052',
'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762',
'rsrc/js/application/conpherence/behavior-durable-column.js' => '2ae077e1',
'rsrc/js/application/conpherence/behavior-menu.js' => '4047cd35',
'rsrc/js/application/conpherence/behavior-participant-pane.js' => 'd057e45a',
'rsrc/js/application/conpherence/behavior-pontificate.js' => '55616e04',
'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3',
'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '3dbf94d5',
'rsrc/js/application/countdown/timer.js' => 'e4cc26b3',
'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145',
'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e',
'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '408bf173',
'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375',
'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63',
'rsrc/js/application/diff/DiffChangeset.js' => 'b49b59d6',
- 'rsrc/js/application/diff/DiffChangesetList.js' => 'e74b7517',
+ 'rsrc/js/application/diff/DiffChangesetList.js' => 'e0b984b5',
'rsrc/js/application/diff/DiffInline.js' => 'e83d28f3',
'rsrc/js/application/diff/behavior-preview-link.js' => '051c7832',
'rsrc/js/application/differential/behavior-comment-preview.js' => '51c5ad07',
'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
- 'rsrc/js/application/differential/behavior-populate.js' => '419998ab',
+ 'rsrc/js/application/differential/behavior-populate.js' => 'f0eb6708',
'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d',
'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '00676f00',
'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' => '75b83cbb',
- 'rsrc/js/application/diffusion/behavior-diffusion-browse-file.js' => '054a0f0b',
'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947',
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc',
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70',
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef',
- 'rsrc/js/application/files/behavior-document-engine.js' => '0333c0b6',
+ 'rsrc/js/application/files/behavior-document-engine.js' => '3935d8c4',
'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab',
'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
- 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909',
+ 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '549459b8',
'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e',
- 'rsrc/js/application/herald/PathTypeahead.js' => '662e9cea',
+ 'rsrc/js/application/herald/PathTypeahead.js' => '6d8c7912',
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ad54037e',
'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876',
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763',
'rsrc/js/application/owners/OwnersPathEditor.js' => 'c96502cf',
'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3',
'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc',
'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => 'bee502c8',
'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'ec1f3669',
'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => 'a6b98425',
'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c',
'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
'rsrc/js/application/projects/WorkboardBoard.js' => '8935deef',
'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f',
'rsrc/js/application/projects/WorkboardColumn.js' => '758b4758',
'rsrc/js/application/projects/WorkboardController.js' => '26167537',
'rsrc/js/application/projects/behavior-project-boards.js' => '4250a34e',
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8',
'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f',
'rsrc/js/application/repository/repository-crossreference.js' => '9a860428',
'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072',
'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08',
'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f',
- 'rsrc/js/application/transactions/behavior-comment-actions.js' => '9a6dd75c',
+ 'rsrc/js/application/transactions/behavior-comment-actions.js' => '038bf27f',
'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'd7a74243',
'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96',
'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '8f29b364',
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6',
'rsrc/js/application/transactions/behavior-transaction-list.js' => '1f6794f6',
'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec',
'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3',
'rsrc/js/application/uiexample/gesture-example.js' => '558829c2',
'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5',
'rsrc/js/core/Busy.js' => '59a7976a',
'rsrc/js/core/DragAndDropFileUpload.js' => '58dea2fa',
'rsrc/js/core/DraggableList.js' => 'bea6e7f4',
'rsrc/js/core/Favicon.js' => '1fe2510c',
'rsrc/js/core/FileUpload.js' => '680ea2c8',
'rsrc/js/core/Hovercard.js' => '1bd28176',
'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9',
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
'rsrc/js/core/Notification.js' => '4f774dac',
'rsrc/js/core/Prefab.js' => '77b0ae28',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '320810c8',
'rsrc/js/core/Title.js' => '485aaa6c',
'rsrc/js/core/ToolTip.js' => '358b8c04',
'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-badge-view.js' => '8ff5e24c',
'rsrc/js/core/behavior-bulk-editor.js' => '66a6def1',
'rsrc/js/core/behavior-choose-control.js' => '327a00d1',
'rsrc/js/core/behavior-copy.js' => 'b0b8f86d',
'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96',
'rsrc/js/core/behavior-device.js' => 'a3714c76',
'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22',
'rsrc/js/core/behavior-fancy-datepicker.js' => 'ecf4e799',
'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' => '960f6a39',
'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03',
'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64',
'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a',
'rsrc/js/core/behavior-line-linker.js' => '66a62306',
'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0',
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
- 'rsrc/js/core/behavior-phabricator-nav.js' => '94b7c320',
+ 'rsrc/js/core/behavior-phabricator-nav.js' => '9d32bc88',
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee',
'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207',
'rsrc/js/core/behavior-redirect.js' => '0213259f',
'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b',
'rsrc/js/core/behavior-remarkup-load-image.js' => '040fce04',
'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e',
'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e',
'rsrc/js/core/behavior-reveal-content.js' => '60821bc7',
'rsrc/js/core/behavior-scrollbar.js' => '834a1173',
'rsrc/js/core/behavior-search-typeahead.js' => 'c3e917d9',
'rsrc/js/core/behavior-select-content.js' => 'bf5374ef',
'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6',
'rsrc/js/core/behavior-setup-check-https.js' => '491416b3',
'rsrc/js/core/behavior-time-typeahead.js' => '522431f7',
'rsrc/js/core/behavior-toggle-class.js' => '92b9ec77',
'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884',
'rsrc/js/core/behavior-tooltip.js' => 'c420b0b9',
'rsrc/js/core/behavior-user-menu.js' => '31420f77',
'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d',
'rsrc/js/core/behavior-workflow.js' => '0a3f3021',
'rsrc/js/core/darkconsole/DarkLog.js' => 'c8e1ffe3',
'rsrc/js/core/darkconsole/DarkMessage.js' => 'c48cccdd',
'rsrc/js/core/darkconsole/behavior-dark-console.js' => '66888767',
'rsrc/js/core/phtize.js' => 'd254d646',
'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d',
'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb',
'rsrc/js/phui/behavior-phui-selectable-list.js' => '464259a2',
'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b',
'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9',
'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
'rsrc/js/phuix/PHUIXActionView.js' => '8d4a8c72',
'rsrc/js/phuix/PHUIXAutocomplete.js' => 'df1bbd34',
- 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac',
+ 'rsrc/js/phuix/PHUIXButtonView.js' => '85ac9772',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03',
'rsrc/js/phuix/PHUIXExample.js' => '68af71ca',
'rsrc/js/phuix/PHUIXFormControl.js' => '210a16c1',
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
),
'symbols' => array(
'almanac-css' => 'dbb9b3af',
'aphront-bars' => '231ac33c',
'aphront-dark-console-css' => '0e14e8f6',
'aphront-dialog-view-css' => '6bfc244b',
'aphront-list-filter-view-css' => '5d6f0526',
'aphront-multi-column-view-css' => '84cc6640',
'aphront-panel-view-css' => '8427b78d',
'aphront-table-view-css' => '8c9bbafe',
'aphront-tokenizer-control-css' => '15d5ff71',
'aphront-tooltip-css' => '173b9431',
'aphront-typeahead-control-css' => 'a4a21016',
'application-search-view-css' => '787f5b76',
'auth-css' => '0877ed6e',
'bulk-job-css' => 'df9c1d4a',
'conduit-api-css' => '7bc725c4',
'config-options-css' => '4615667b',
'conpherence-color-css' => 'abb4c358',
'conpherence-durable-column-view' => '89ea6bef',
'conpherence-header-pane-css' => 'cb6f4e19',
'conpherence-menu-css' => '69368e97',
'conpherence-message-pane-css' => 'b0f55ecc',
'conpherence-notification-css' => 'cef0a3fc',
'conpherence-participant-pane-css' => '26a3ce56',
'conpherence-thread-manager' => '4d863052',
'conpherence-transaction-css' => '85129c68',
'd3' => 'a11a5ff2',
'differential-changeset-view-css' => 'db34a142',
'differential-core-view-css' => '5b7b8ff4',
'differential-revision-add-comment-css' => 'c47f8c40',
'differential-revision-comment-css' => '14b8565a',
'differential-revision-history-css' => '0e8eb855',
'differential-revision-list-css' => 'f3c47d33',
'differential-table-of-contents-css' => 'ae4b7a55',
'diffusion-css' => '45727264',
'diffusion-icons-css' => '0c15255e',
'diffusion-readme-css' => '419dd5b6',
'diffusion-repository-css' => 'ee6f20ec',
'diviner-shared-css' => '896f1d43',
'font-fontawesome' => 'e838e088',
'font-lato' => 'c7ccd872',
'global-drag-and-drop-css' => 'b556a948',
- 'harbormaster-css' => '730a4a3c',
+ 'harbormaster-css' => '7446ce72',
'herald-css' => 'cd8d0134',
'herald-rule-editor' => 'dca75c0e',
'herald-test-css' => 'a52e323e',
'inline-comment-summary-css' => 'f23d4e8f',
'javelin-aphlict' => 'e1d4b11a',
'javelin-behavior' => '61cbc29a',
'javelin-behavior-aphlict-dropdown' => 'caade6f2',
'javelin-behavior-aphlict-listen' => '599a8f5f',
'javelin-behavior-aphlict-status' => '5e2634b9',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22',
'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-badge-view' => '8ff5e24c',
'javelin-behavior-bulk-editor' => '66a6def1',
'javelin-behavior-bulk-job-reload' => 'edf8a145',
'javelin-behavior-calendar-month-view' => 'fe33e256',
'javelin-behavior-choose-control' => '327a00d1',
- 'javelin-behavior-comment-actions' => '9a6dd75c',
+ 'javelin-behavior-comment-actions' => '038bf27f',
'javelin-behavior-config-reorder-fields' => 'b6993408',
'javelin-behavior-conpherence-menu' => '4047cd35',
'javelin-behavior-conpherence-participant-pane' => 'd057e45a',
'javelin-behavior-conpherence-pontificate' => '55616e04',
'javelin-behavior-conpherence-search' => '9bbf3762',
'javelin-behavior-countdown-timer' => 'e4cc26b3',
'javelin-behavior-dark-console' => '66888767',
'javelin-behavior-dashboard-async-panel' => '469c0d9e',
'javelin-behavior-dashboard-move-panels' => '408bf173',
'javelin-behavior-dashboard-query-panel-select' => '453c5375',
'javelin-behavior-dashboard-tab-panel' => 'd4eecc63',
'javelin-behavior-day-view' => '4b3c4443',
'javelin-behavior-desktop-notifications-control' => '27ca6289',
'javelin-behavior-detect-timezone' => '4c193c96',
'javelin-behavior-device' => 'a3714c76',
'javelin-behavior-diff-preview-link' => '051c7832',
'javelin-behavior-differential-diff-radios' => 'e1ff79b1',
'javelin-behavior-differential-feedback-preview' => '51c5ad07',
- 'javelin-behavior-differential-populate' => '419998ab',
+ 'javelin-behavior-differential-populate' => 'f0eb6708',
'javelin-behavior-differential-user-select' => 'a8d8459d',
- 'javelin-behavior-diffusion-browse-file' => '054a0f0b',
'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
'javelin-behavior-diffusion-commit-graph' => '75b83cbb',
'javelin-behavior-diffusion-locate-file' => '6d3e1947',
'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc',
- 'javelin-behavior-document-engine' => '0333c0b6',
+ 'javelin-behavior-document-engine' => '3935d8c4',
'javelin-behavior-doorkeeper-tag' => '1db13e70',
'javelin-behavior-drydock-live-operation-status' => '901935ef',
'javelin-behavior-durable-column' => '2ae077e1',
'javelin-behavior-editengine-reorder-configs' => 'd7a74243',
'javelin-behavior-editengine-reorder-fields' => 'b59e1e96',
'javelin-behavior-event-all-day' => 'b41537c9',
'javelin-behavior-fancy-datepicker' => 'ecf4e799',
'javelin-behavior-global-drag-and-drop' => '960f6a39',
- 'javelin-behavior-harbormaster-log' => '191b4909',
+ 'javelin-behavior-harbormaster-log' => '549459b8',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => 'a464fe03',
'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8499b6ab',
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => '6b31879a',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
'javelin-behavior-maniphest-subpriority-editor' => '71237763',
'javelin-behavior-owners-path-editor' => '7a68dda3',
'javelin-behavior-passphrase-credential-control' => '3cb0b2fc',
'javelin-behavior-phabricator-active-nav' => 'e379b58e',
'javelin-behavior-phabricator-autofocus' => '7319e029',
'javelin-behavior-phabricator-clipboard-copy' => 'b0b8f86d',
'javelin-behavior-phabricator-file-tree' => '88236f00',
'javelin-behavior-phabricator-gesture' => '3ab51e2c',
'javelin-behavior-phabricator-gesture-example' => '558829c2',
'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0',
'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0',
'javelin-behavior-phabricator-line-linker' => '66a62306',
- 'javelin-behavior-phabricator-nav' => '94b7c320',
+ 'javelin-behavior-phabricator-nav' => '9d32bc88',
'javelin-behavior-phabricator-notification-example' => '8ce821c5',
'javelin-behavior-phabricator-object-selector' => '77c1f0b0',
'javelin-behavior-phabricator-oncopy' => '2926fff2',
'javelin-behavior-phabricator-remarkup-assist' => 'acd29eee',
'javelin-behavior-phabricator-reveal-content' => '60821bc7',
'javelin-behavior-phabricator-search-typeahead' => 'c3e917d9',
'javelin-behavior-phabricator-show-older-transactions' => '8f29b364',
'javelin-behavior-phabricator-tooltips' => 'c420b0b9',
'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6',
'javelin-behavior-phabricator-transaction-list' => '1f6794f6',
'javelin-behavior-phabricator-watch-anchor' => '9f36c42d',
'javelin-behavior-pholio-mock-edit' => 'bee502c8',
'javelin-behavior-pholio-mock-view' => 'ec1f3669',
'javelin-behavior-phui-dropdown-menu' => 'b95d6f7d',
'javelin-behavior-phui-file-upload' => 'b003d4fb',
'javelin-behavior-phui-hovercards' => 'bcaccd64',
'javelin-behavior-phui-selectable-list' => '464259a2',
'javelin-behavior-phui-submenu' => 'a6f7a73b',
'javelin-behavior-phui-tab-group' => '0a0b10e9',
'javelin-behavior-phuix-example' => '68af71ca',
'javelin-behavior-policy-control' => 'd0c516d5',
'javelin-behavior-policy-rule-editor' => '5e9f347c',
'javelin-behavior-project-boards' => '4250a34e',
'javelin-behavior-project-create' => '065227cc',
'javelin-behavior-quicksand-blacklist' => '7927a7d3',
'javelin-behavior-read-only-warning' => 'ba158207',
'javelin-behavior-redirect' => '0213259f',
'javelin-behavior-refresh-csrf' => 'ab2f381b',
'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf',
'javelin-behavior-releeph-request-state-change' => 'a0b57eb8',
'javelin-behavior-releeph-request-typeahead' => 'de2e896f',
'javelin-behavior-remarkup-load-image' => '040fce04',
'javelin-behavior-remarkup-preview' => '4b700e9e',
'javelin-behavior-reorder-applications' => '76b9fc3e',
'javelin-behavior-reorder-columns' => 'e1d25dfb',
'javelin-behavior-reorder-profile-menu-items' => 'e2e0a072',
'javelin-behavior-repository-crossreference' => '9a860428',
'javelin-behavior-scrollbar' => '834a1173',
'javelin-behavior-search-reorder-queries' => 'e9581f08',
'javelin-behavior-select-content' => 'bf5374ef',
'javelin-behavior-select-on-click' => '4e3e79a6',
'javelin-behavior-setup-check-https' => '491416b3',
'javelin-behavior-slowvote-embed' => '887ad43f',
'javelin-behavior-stripe-payment-form' => 'a6b98425',
'javelin-behavior-test-payment-form' => 'fc91ab6c',
'javelin-behavior-time-typeahead' => '522431f7',
'javelin-behavior-toggle-class' => '92b9ec77',
'javelin-behavior-toggle-widget' => '3dbf94d5',
'javelin-behavior-typeahead-browse' => '635de1ec',
'javelin-behavior-typeahead-search' => '93d0c9e3',
'javelin-behavior-user-menu' => '31420f77',
'javelin-behavior-view-placeholder' => '47830651',
'javelin-behavior-workflow' => '0a3f3021',
'javelin-color' => '7e41274a',
'javelin-cookie' => '62dfea03',
'javelin-diffusion-locate-file-source' => '00676f00',
'javelin-dom' => '4976858c',
'javelin-dynval' => 'f6555212',
'javelin-event' => '2ee659ce',
'javelin-fx' => '54b612ba',
'javelin-history' => 'd4505101',
'javelin-install' => '05270951',
'javelin-json' => '69adf288',
'javelin-leader' => '7f243deb',
'javelin-magical-init' => '638a4e2b',
'javelin-mask' => '8a41885b',
'javelin-quicksand' => '6b8ef10b',
'javelin-reactor' => '2b8de964',
'javelin-reactor-dom' => 'c90a04fc',
'javelin-reactor-node-calmer' => '76f4ebed',
'javelin-reactornode' => '1ad0a787',
'javelin-request' => '94b750d2',
'javelin-resource' => '44959b73',
'javelin-routable' => 'b3e7d692',
'javelin-router' => '29274e2b',
'javelin-scrollbar' => '9065f639',
'javelin-sound' => '949c0fe5',
'javelin-stratcom' => '327f418a',
- 'javelin-tokenizer' => '8d3bc1b2',
+ 'javelin-tokenizer' => 'bb6e5c16',
'javelin-typeahead' => '70baed2f',
'javelin-typeahead-composite-source' => '503e17fd',
'javelin-typeahead-normalizer' => '185bbd53',
'javelin-typeahead-ondemand-source' => '013ffff9',
'javelin-typeahead-preloaded-source' => '54f314a0',
'javelin-typeahead-source' => 'ab9e0a82',
'javelin-typeahead-static-source' => '6c0e62fa',
'javelin-uri' => 'c989ade3',
'javelin-util' => '93cc50d6',
'javelin-vector' => '2caa8fb8',
'javelin-view' => '0f764c35',
'javelin-view-html' => 'fe287620',
'javelin-view-interpreter' => 'f829edb3',
'javelin-view-renderer' => '6c2b09a2',
'javelin-view-visitor' => 'efe49472',
'javelin-websocket' => '3ffe32d6',
'javelin-workboard-board' => '8935deef',
'javelin-workboard-card' => 'c587b80f',
'javelin-workboard-column' => '758b4758',
'javelin-workboard-controller' => '26167537',
'javelin-workflow' => '6a726c55',
- 'katex-css' => '297123ca',
'maniphest-report-css' => '9b9580b7',
'maniphest-task-edit-css' => 'fda62a9b',
'maniphest-task-summary-css' => '11cc5344',
'multirow-row-manager' => 'b5d57730',
'owners-path-editor' => 'c96502cf',
'owners-path-editor-css' => '9c136c29',
'paste-css' => '9fcc9773',
- 'path-typeahead' => '662e9cea',
+ 'path-typeahead' => '6d8c7912',
'people-picture-menu-item-css' => 'a06f7f34',
'people-profile-css' => '4df76faf',
'phabricator-action-list-view-css' => '0bcd9a45',
'phabricator-busy' => '59a7976a',
'phabricator-chatlog-css' => 'd295b020',
'phabricator-content-source-view-css' => '4b8b05d4',
'phabricator-core-css' => '62fa3ace',
'phabricator-countdown-css' => '16c52f5c',
'phabricator-darklog' => 'c8e1ffe3',
'phabricator-darkmessage' => 'c48cccdd',
'phabricator-dashboard-css' => 'fe5b1869',
'phabricator-diff-changeset' => 'b49b59d6',
- 'phabricator-diff-changeset-list' => 'e74b7517',
+ 'phabricator-diff-changeset-list' => 'e0b984b5',
'phabricator-diff-inline' => 'e83d28f3',
'phabricator-drag-and-drop-file-upload' => '58dea2fa',
'phabricator-draggable-list' => 'bea6e7f4',
'phabricator-fatal-config-template-css' => '8f18fa41',
'phabricator-favicon' => '1fe2510c',
'phabricator-feed-css' => 'ecd4ec57',
'phabricator-file-upload' => '680ea2c8',
'phabricator-filetree-view-css' => 'b912ad97',
'phabricator-flag-css' => 'bba8f811',
'phabricator-keyboard-shortcut' => '1ae869f2',
'phabricator-keyboard-shortcut-manager' => 'c19dd9b9',
'phabricator-main-menu-view' => '1802a242',
'phabricator-nav-view-css' => '694d7723',
'phabricator-notification' => '4f774dac',
'phabricator-notification-css' => '457861ec',
- 'phabricator-notification-menu-css' => '10685bd4',
+ 'phabricator-notification-menu-css' => 'ef480927',
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '77b0ae28',
- 'phabricator-remarkup-css' => '7f9ea8b0',
+ 'phabricator-remarkup-css' => 'b182076e',
'phabricator-search-results-css' => '505dd8cf',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
- 'phabricator-source-code-view-css' => '09368218',
+ 'phabricator-source-code-view-css' => '2ab25dfa',
'phabricator-standard-page-view' => '34ee718b',
'phabricator-textareautils' => '320810c8',
'phabricator-title' => '485aaa6c',
'phabricator-tooltip' => '358b8c04',
'phabricator-ui-example-css' => '528b19de',
'phabricator-zindex-css' => '9d8f7c4b',
'phame-css' => '8cb3afcd',
'pholio-css' => 'ca89d380',
'pholio-edit-css' => '07676f51',
'pholio-inline-comments-css' => '8e545e49',
'phortune-credit-card-form' => '2290aeef',
'phortune-credit-card-form-css' => '8391eb02',
'phortune-css' => '5b99dae0',
'phortune-invoice-css' => '476055e2',
'phrequent-css' => 'ffc185ad',
'phriction-document-css' => '4282e4ad',
'phui-action-panel-css' => 'b4798122',
'phui-badge-view-css' => '22c0cf4f',
'phui-basic-nav-view-css' => '98c11ab3',
'phui-big-info-view-css' => 'acc3492c',
'phui-box-css' => '4bd6cdb9',
'phui-bulk-editor-css' => '9a81e5d5',
'phui-button-bar-css' => 'f1ff5494',
- 'phui-button-css' => '1863cc6e',
+ 'phui-button-css' => '6ccb303c',
'phui-button-simple-css' => '8e1baf68',
'phui-calendar-css' => 'f1ddf11c',
'phui-calendar-day-css' => '572b1893',
'phui-calendar-list-css' => '576be600',
'phui-calendar-month-css' => '21154caf',
'phui-chart-css' => '6bf6f78e',
'phui-cms-css' => '504b4b23',
'phui-comment-form-css' => 'ac68149f',
'phui-comment-panel-css' => 'f50152ad',
- 'phui-crumbs-view-css' => '6ece3bbb',
+ 'phui-crumbs-view-css' => '10728aaa',
'phui-curtain-view-css' => '2bdaf026',
'phui-document-summary-view-css' => '9ca48bdf',
- 'phui-document-view-css' => '391e1e84',
- 'phui-document-view-pro-css' => '8af7ea27',
+ 'phui-document-view-css' => 'c4ac41f9',
+ 'phui-document-view-pro-css' => 'd033e8d5',
'phui-feed-story-css' => '44a9c8e9',
'phui-font-icon-base-css' => '870a7360',
'phui-fontkit-css' => '1320ed01',
'phui-form-css' => '7aaa04e3',
- 'phui-form-view-css' => 'ae9f8d16',
+ 'phui-form-view-css' => '2f43fae7',
'phui-head-thing-view-css' => 'fd311e5f',
- 'phui-header-view-css' => '31dc6c72',
+ 'phui-header-view-css' => '1ba8b707',
'phui-hovercard' => '1bd28176',
'phui-hovercard-view-css' => 'f0592bcf',
'phui-icon-set-selector-css' => '87db8fee',
'phui-icon-view-css' => 'cf24ceec',
'phui-image-mask-css' => 'a8498f9c',
'phui-info-view-css' => 'e929f98c',
'phui-inline-comment-view-css' => '65ae3bc2',
'phui-invisible-character-view-css' => '6993d9f0',
'phui-left-right-css' => '75227a4d',
'phui-lightbox-css' => '0a035e40',
'phui-list-view-css' => '38f8c9bd',
'phui-object-box-css' => '9cff003c',
'phui-oi-big-ui-css' => '628f59de',
'phui-oi-color-css' => 'cd2b9b77',
'phui-oi-drag-ui-css' => '08f4ccc3',
'phui-oi-flush-ui-css' => '9d9685d6',
- 'phui-oi-list-view-css' => 'ae1404ba',
+ 'phui-oi-list-view-css' => '7c5c1291',
'phui-oi-simple-ui-css' => 'a8beebea',
'phui-pager-css' => 'edcbc226',
'phui-pinboard-view-css' => '2495140e',
- 'phui-property-list-view-css' => 'de4754d8',
+ 'phui-property-list-view-css' => '546a04ae',
'phui-remarkup-preview-css' => '54a34863',
'phui-segment-bar-view-css' => 'b1d1b892',
'phui-spacing-css' => '042804d6',
'phui-status-list-view-css' => 'd5263e49',
'phui-tag-view-css' => 'b4719c50',
'phui-theme-css' => '9f261c6b',
'phui-timeline-view-css' => '6ddf8126',
'phui-two-column-view-css' => '44ec4951',
'phui-workboard-color-css' => '783cdff5',
'phui-workboard-view-css' => '3bc85455',
'phui-workcard-view-css' => 'cca5fa92',
'phui-workpanel-view-css' => 'a3a63478',
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => '8d4a8c72',
'phuix-autocomplete' => 'df1bbd34',
- 'phuix-button-view' => '8a91e1ac',
+ 'phuix-button-view' => '85ac9772',
'phuix-dropdown-menu' => '04b2ae03',
'phuix-form-control-view' => '210a16c1',
'phuix-icon-view' => 'bff6884b',
'policy-css' => '957ea14c',
'policy-edit-css' => '815c66f7',
'policy-transaction-detail-css' => '82100a43',
'ponder-view-css' => 'fbd45f96',
'project-card-view-css' => '0010bb52',
'project-view-css' => '792c9057',
'releeph-core' => '9b3c5733',
'releeph-preview-branch' => 'b7a6f4a5',
'releeph-request-differential-create-dialog' => '8d8b92cd',
'releeph-request-typeahead-css' => '667a48ae',
'setup-issue-css' => '30ee0173',
'sprite-login-css' => '396f3c3a',
'sprite-tokens-css' => '9cdfd599',
'syntax-default-css' => '9923583c',
'syntax-highlighting-css' => 'e9c95dd4',
'tokens-css' => '3d0f239e',
'typeahead-browse-css' => 'f2818435',
'unhandled-exception-css' => '4c96257a',
),
'requires' => array(
'00676f00' => array(
'javelin-install',
'javelin-dom',
'javelin-typeahead-preloaded-source',
'javelin-util',
),
'013ffff9' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-typeahead-source',
),
'01fca1f0' => array(
'javelin-behavior',
'javelin-workflow',
'javelin-json',
'javelin-dom',
'phabricator-keyboard-shortcut',
),
'0213259f' => array(
'javelin-behavior',
'javelin-uri',
),
- '0333c0b6' => array(
+ '038bf27f' => array(
'javelin-behavior',
- 'javelin-dom',
'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-dom',
+ 'phuix-form-control-view',
+ 'phuix-icon-view',
+ 'javelin-behavior-phabricator-gesture',
),
'040fce04' => array(
'javelin-behavior',
'javelin-request',
),
'04b2ae03' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
'javelin-stratcom',
),
'051c7832' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'05270951' => array(
'javelin-util',
'javelin-magical-init',
),
- '054a0f0b' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-util',
- 'phabricator-tooltip',
- ),
'065227cc' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
),
'08f4ccc3' => array(
'phui-oi-list-view-css',
),
'0a0b10e9' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'0a3f3021' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'javelin-router',
),
'0f764c35' => array(
'javelin-install',
'javelin-util',
),
'15d5ff71' => array(
'aphront-typeahead-control-css',
'phui-tag-view-css',
),
'1802a242' => array(
'phui-theme-css',
),
'185bbd53' => array(
'javelin-install',
),
- '191b4909' => array(
- 'javelin-behavior',
- ),
'1ad0a787' => array(
'javelin-install',
'javelin-reactor',
'javelin-util',
'javelin-reactor-node-calmer',
),
'1ae869f2' => array(
'javelin-install',
'javelin-util',
'phabricator-keyboard-shortcut-manager',
),
'1bd28176' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'1db13e70' => array(
'javelin-behavior',
'javelin-dom',
'javelin-json',
'javelin-workflow',
'javelin-magical-init',
),
'1f6794f6' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'javelin-uri',
'phabricator-textareautils',
),
'1fe2510c' => array(
'javelin-install',
'javelin-dom',
),
'210a16c1' => array(
'javelin-install',
'javelin-dom',
),
'2290aeef' => array(
'javelin-install',
'javelin-dom',
'javelin-json',
'javelin-workflow',
'javelin-util',
),
26167537 => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board',
),
'27ca6289' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-uri',
'phabricator-notification',
),
'2926fff2' => array(
'javelin-behavior',
'javelin-dom',
),
'29274e2b' => array(
'javelin-install',
'javelin-util',
),
- '297123ca' => array(
- 'phui-fontkit-css',
- ),
'2ae077e1' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-behavior-device',
'javelin-scrollbar',
'javelin-quicksand',
'phabricator-keyboard-shortcut',
'conpherence-thread-manager',
),
'2b8de964' => array(
'javelin-install',
'javelin-util',
),
'2caa8fb8' => array(
'javelin-install',
'javelin-event',
),
'2ee659ce' => array(
'javelin-install',
),
'31420f77' => array(
'javelin-behavior',
),
'320810c8' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
),
'327a00d1' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-workflow',
),
'327f418a' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'358b8c04' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
),
- '391e1e84' => array(
- 'katex-css',
+ '3935d8c4' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-stratcom',
),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-magical-init',
),
'3cb0b2fc' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'javelin-uri',
),
'3dbf94d5' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
),
'3ffe32d6' => array(
'javelin-install',
),
'4047cd35' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'javelin-behavior-device',
'javelin-history',
'javelin-vector',
'javelin-scrollbar',
'phabricator-title',
'phabricator-shaped-request',
'conpherence-thread-manager',
),
'408bf173' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
- '419998ab' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'phabricator-tooltip',
- 'phabricator-diff-changeset-list',
- 'phabricator-diff-changeset',
- ),
'4250a34e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
),
'44959b73' => array(
'javelin-util',
'javelin-uri',
'javelin-install',
),
'453c5375' => array(
'javelin-behavior',
'javelin-dom',
),
'464259a2' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'469c0d9e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
),
47830651 => array(
'javelin-behavior',
'javelin-dom',
'javelin-view-renderer',
'javelin-install',
),
48086888 => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
),
'484a6e22' => array(
'javelin-behavior',
'javelin-dom',
'phabricator-drag-and-drop-file-upload',
'phabricator-textareautils',
),
'485aaa6c' => array(
'javelin-install',
),
'491416b3' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
'4976858c' => array(
'javelin-magical-init',
'javelin-install',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
),
'4b3c4443' => array(
'phuix-icon-view',
),
'4b700e9e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
'4c193c96' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
'4d863052' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-aphlict',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
),
'4e3e79a6' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'4f774dac' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'503e17fd' => array(
'javelin-install',
'javelin-typeahead-source',
'javelin-util',
),
'51c5ad07' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-request',
'javelin-util',
'phabricator-shaped-request',
),
'522431f7' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
'javelin-typeahead-static-source',
),
+ '549459b8' => array(
+ 'javelin-behavior',
+ ),
'54b612ba' => array(
'javelin-color',
'javelin-install',
'javelin-util',
),
'54f314a0' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-typeahead-source',
),
'55616e04' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
'conpherence-thread-manager',
),
'558829c2' => array(
'javelin-stratcom',
'javelin-behavior',
'javelin-vector',
'javelin-dom',
),
'58dea2fa' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-dom',
'javelin-uri',
'phabricator-file-upload',
),
'599a8f5f' => array(
'javelin-behavior',
'javelin-aphlict',
'javelin-stratcom',
'javelin-request',
'javelin-uri',
'javelin-dom',
'javelin-json',
'javelin-router',
'javelin-util',
'javelin-leader',
'javelin-sound',
'phabricator-notification',
),
'59a7976a' => array(
'javelin-install',
'javelin-dom',
'javelin-fx',
),
'59b251eb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
),
'5c54cbf3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'5e2634b9' => array(
'javelin-behavior',
'javelin-aphlict',
'phabricator-phtize',
'javelin-dom',
),
'5e9f347c' => array(
'javelin-behavior',
'multirow-row-manager',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
'javelin-json',
),
'60821bc7' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'61cbc29a' => array(
'javelin-magical-init',
'javelin-util',
),
'628f59de' => array(
'phui-oi-list-view-css',
),
'62dfea03' => array(
'javelin-install',
'javelin-util',
),
'635de1ec' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
),
- '662e9cea' => array(
- 'javelin-install',
- 'javelin-typeahead',
- 'javelin-dom',
- 'javelin-request',
- 'javelin-typeahead-ondemand-source',
- 'javelin-util',
- ),
66888767 => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-util',
'javelin-dom',
'javelin-request',
'phabricator-keyboard-shortcut',
'phabricator-darklog',
'phabricator-darkmessage',
),
'66a62306' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'66a6def1' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'multirow-row-manager',
'javelin-json',
'phuix-form-control-view',
),
'680ea2c8' => array(
'javelin-install',
'javelin-dom',
'phabricator-notification',
),
'68af71ca' => array(
'javelin-install',
'javelin-dom',
'phuix-button-view',
),
'69adf288' => array(
'javelin-install',
),
'6a726c55' => array(
'javelin-stratcom',
'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
'javelin-util',
'javelin-mask',
'javelin-uri',
'javelin-routable',
),
'6b31879a' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'6b8ef10b' => array(
'javelin-install',
),
'6c0e62fa' => array(
'javelin-install',
'javelin-typeahead-source',
),
'6c2b09a2' => array(
'javelin-install',
'javelin-util',
),
'6d3e1947' => array(
'javelin-behavior',
'javelin-diffusion-locate-file-source',
'javelin-dom',
'javelin-typeahead',
'javelin-uri',
),
+ '6d8c7912' => array(
+ 'javelin-install',
+ 'javelin-typeahead',
+ 'javelin-dom',
+ 'javelin-request',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-util',
+ ),
'70baed2f' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-util',
),
71237763 => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
'7319e029' => array(
'javelin-behavior',
'javelin-dom',
),
'758b4758' => array(
'javelin-install',
'javelin-workboard-card',
),
'75b83cbb' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'76b9fc3e' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'76f4ebed' => array(
'javelin-install',
'javelin-reactor',
'javelin-util',
),
'77b0ae28' => 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',
),
'77c1f0b0' => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
'javelin-util',
),
'7927a7d3' => array(
'javelin-behavior',
'javelin-quicksand',
),
'7a68dda3' => array(
'owners-path-editor',
'javelin-behavior',
),
'7cbe244b' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-router',
),
'7e41274a' => array(
'javelin-install',
),
'7ebaeed3' => array(
'herald-rule-editor',
'javelin-behavior',
),
'7ee2b591' => array(
'javelin-behavior',
'javelin-history',
),
'7f243deb' => array(
'javelin-install',
),
- '7f9ea8b0' => array(
- 'katex-css',
- ),
'834a1173' => array(
'javelin-behavior',
'javelin-scrollbar',
),
'8499b6ab' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
+ '85ac9772' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ ),
'85ee8ce6' => array(
'aphront-dialog-view-css',
),
'88236f00' => array(
'javelin-behavior',
'phabricator-keyboard-shortcut',
'javelin-stratcom',
),
'887ad43f' => array(
'javelin-behavior',
'javelin-request',
'javelin-stratcom',
'javelin-dom',
),
'8935deef' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'javelin-workboard-column',
),
'8a41885b' => array(
'javelin-install',
'javelin-dom',
),
- '8a91e1ac' => array(
- 'javelin-install',
- 'javelin-dom',
- ),
'8ce821c5' => array(
'phabricator-notification',
'javelin-stratcom',
'javelin-behavior',
),
- '8d3bc1b2' => array(
- 'javelin-dom',
- 'javelin-util',
- 'javelin-stratcom',
- 'javelin-install',
- ),
'8d4a8c72' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
),
'8e1baf68' => array(
'phui-button-css',
),
'8f29b364' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-busy',
),
'8ff5e24c' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'901935ef' => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
),
'9065f639' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'92b9ec77' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'93d0c9e3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
),
'949c0fe5' => array(
'javelin-install',
),
'94b750d2' => array(
'javelin-install',
'javelin-stratcom',
'javelin-util',
'javelin-behavior',
'javelin-json',
'javelin-dom',
'javelin-resource',
'javelin-routable',
),
- '94b7c320' => array(
- 'javelin-behavior',
- 'javelin-behavior-device',
- 'javelin-stratcom',
- 'javelin-dom',
- 'javelin-magical-init',
- 'javelin-vector',
- 'javelin-request',
- 'javelin-util',
- ),
'960f6a39' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-mask',
'phabricator-drag-and-drop-file-upload',
),
- '9a6dd75c' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-dom',
- 'phuix-form-control-view',
- 'phuix-icon-view',
- 'javelin-behavior-phabricator-gesture',
- ),
'9a860428' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-uri',
),
'9bbf3762' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
),
+ '9d32bc88' => array(
+ 'javelin-behavior',
+ 'javelin-behavior-device',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ 'javelin-magical-init',
+ 'javelin-vector',
+ 'javelin-request',
+ 'javelin-util',
+ ),
'9d9685d6' => array(
'phui-oi-list-view-css',
),
'9f36c42d' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
),
'a0b57eb8' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-keyboard-shortcut',
),
'a3714c76' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
'javelin-install',
),
'a3a63478' => array(
'phui-workcard-view-css',
),
'a464fe03' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
'a6b98425' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'a6f7a73b' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'a80d0378' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'a8beebea' => array(
'phui-oi-list-view-css',
),
'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',
),
'ab2f381b' => array(
'javelin-request',
'javelin-behavior',
'javelin-dom',
'javelin-router',
'javelin-util',
'phabricator-busy',
),
'ab9e0a82' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-typeahead-normalizer',
),
'acd29eee' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-phtize',
'phabricator-textareautils',
'javelin-workflow',
'javelin-vector',
'phuix-autocomplete',
'javelin-mask',
),
'ad54037e' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
),
'b003d4fb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
'b0b8f86d' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'b23b49e6' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-request',
'phabricator-shaped-request',
),
'b2b4fbaf' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-request',
),
'b3a4b884' => array(
'javelin-behavior',
'phabricator-prefab',
),
'b3e7d692' => array(
'javelin-install',
),
'b49b59d6' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
'phabricator-diff-inline',
),
'b59e1e96' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'b5c256b8' => array(
'javelin-install',
'javelin-dom',
),
'b5d57730' => array(
'javelin-install',
'javelin-stratcom',
'javelin-dom',
'javelin-util',
),
'b6993408' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-json',
'phabricator-draggable-list',
),
'b95d6f7d' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
'ba158207' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
+ 'bb6e5c16' => array(
+ 'javelin-dom',
+ 'javelin-util',
+ 'javelin-stratcom',
+ 'javelin-install',
+ ),
'bcaccd64' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phui-hovercard',
),
'bdaf4d04' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-request',
),
'bea6e7f4' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'javelin-vector',
'javelin-magical-init',
),
'bee502c8' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-workflow',
'javelin-quicksand',
'phabricator-phtize',
'phabricator-drag-and-drop-file-upload',
'phabricator-draggable-list',
),
'bf5374ef' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'bff6884b' => array(
'javelin-install',
'javelin-dom',
),
'c19dd9b9' => array(
'javelin-install',
'javelin-util',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
),
'c3e917d9' => array(
'javelin-behavior',
'javelin-typeahead-ondemand-source',
'javelin-typeahead',
'javelin-dom',
'javelin-uri',
'javelin-util',
'javelin-stratcom',
'phabricator-prefab',
'phuix-icon-view',
),
'c420b0b9' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'phabricator-tooltip',
),
'c587b80f' => array(
'javelin-install',
),
'c7ccd872' => array(
'phui-fontkit-css',
),
'c90a04fc' => array(
'javelin-dom',
'javelin-dynval',
'javelin-reactor',
'javelin-reactornode',
'javelin-install',
'javelin-util',
),
'c96502cf' => array(
'multirow-row-manager',
'javelin-install',
'path-typeahead',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
'phuix-form-control-view',
),
'c989ade3' => array(
'javelin-install',
'javelin-util',
'javelin-stratcom',
),
'caade6f2' => array(
'javelin-behavior',
'javelin-request',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
'javelin-behavior-device',
'phabricator-title',
'phabricator-favicon',
),
'cd2b9b77' => array(
'phui-oi-list-view-css',
),
'd057e45a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-notification',
'conpherence-thread-manager',
),
'd0c516d5' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'javelin-workflow',
'phuix-icon-view',
),
'd254d646' => array(
'javelin-util',
),
'd4505101' => array(
'javelin-stratcom',
'javelin-install',
'javelin-uri',
'javelin-util',
),
'd4eecc63' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'd7a74243' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'd835b03a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
'db34a142' => array(
'phui-inline-comment-view-css',
),
'dca75c0e' => array(
'multirow-row-manager',
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-json',
'phabricator-prefab',
),
'de2e896f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-typeahead',
'javelin-typeahead-ondemand-source',
'javelin-dom',
),
'df1bbd34' => array(
'javelin-install',
'javelin-dom',
'phuix-icon-view',
'phabricator-prefab',
),
+ 'e0b984b5' => array(
+ 'javelin-install',
+ 'phuix-button-view',
+ ),
'e1d25dfb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'e1d4b11a' => array(
'javelin-install',
'javelin-util',
'javelin-websocket',
'javelin-leader',
'javelin-json',
),
'e1ff79b1' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'e2e0a072' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'e379b58e' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
),
'e4232876' => array(
'javelin-behavior',
'javelin-dom',
'javelin-vector',
'phui-chart-css',
),
'e4cc26b3' => array(
'javelin-behavior',
'javelin-dom',
),
- 'e74b7517' => array(
- 'javelin-install',
- 'phuix-button-view',
- ),
'e83d28f3' => array(
'javelin-dom',
),
'e9581f08' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'e9c95dd4' => array(
'syntax-default-css',
),
'ec1f3669' => 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',
),
'ecf4e799' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'edf8a145' => array(
'javelin-behavior',
'javelin-uri',
),
'efe49472' => array(
'javelin-install',
'javelin-util',
),
'f01586dc' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-json',
),
+ 'f0eb6708' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'phabricator-tooltip',
+ 'phabricator-diff-changeset-list',
+ 'phabricator-diff-changeset',
+ ),
'f1ff5494' => array(
'phui-button-css',
'phui-button-simple-css',
),
'f50152ad' => array(
'phui-timeline-view-css',
),
'f6555212' => array(
'javelin-install',
'javelin-reactornode',
'javelin-util',
'javelin-reactor',
),
'f829edb3' => array(
'javelin-view',
'javelin-install',
'javelin-dom',
),
'fc91ab6c' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'fe287620' => array(
'javelin-install',
'javelin-dom',
'javelin-view-visitor',
'javelin-util',
),
),
'packages' => array(
'conpherence.pkg.css' => array(
'conpherence-durable-column-view',
'conpherence-menu-css',
'conpherence-color-css',
'conpherence-message-pane-css',
'conpherence-notification-css',
'conpherence-transaction-css',
'conpherence-participant-pane-css',
'conpherence-header-pane-css',
),
'conpherence.pkg.js' => array(
'javelin-behavior-conpherence-menu',
'javelin-behavior-conpherence-participant-pane',
'javelin-behavior-conpherence-pontificate',
'javelin-behavior-toggle-widget',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phui-button-simple-css',
'phui-theme-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'application-search-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'syntax-default-css',
'phui-pager-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'phui-info-view-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',
'phui-crumbs-view-css',
'phui-oi-list-view-css',
'phui-oi-color-css',
'phui-oi-big-ui-css',
'phui-oi-drag-ui-css',
'phui-oi-simple-ui-css',
'phui-oi-flush-ui-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'font-lato',
'phui-font-icon-base-css',
'phui-fontkit-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'phui-two-column-view-css',
'phui-curtain-view-css',
'sprite-login-css',
'sprite-tokens-css',
'tokens-css',
'auth-css',
'phui-status-list-view-css',
'phui-feed-story-css',
'phabricator-feed-css',
'phabricator-dashboard-css',
'aphront-multi-column-view-css',
),
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phuix-icon-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-sound',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phui-hovercard',
'javelin-behavior-phui-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-older-transactions',
'javelin-behavior-phui-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
'phabricator-title',
'javelin-leader',
'javelin-websocket',
'javelin-behavior-dashboard-async-panel',
'javelin-behavior-dashboard-tab-panel',
'javelin-quicksand',
'javelin-behavior-quicksand-blacklist',
'javelin-behavior-high-security-warning',
'javelin-behavior-read-only-warning',
'javelin-scrollbar',
'javelin-behavior-scrollbar',
'javelin-behavior-durable-column',
'conpherence-thread-manager',
'javelin-behavior-detect-timezone',
'javelin-behavior-setup-check-https',
'javelin-behavior-aphlict-status',
'javelin-behavior-user-menu',
'phabricator-favicon',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'phabricator-filetree-view-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',
'phabricator-diff-changeset-list',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
),
);
diff --git a/resources/sql/autopatches/20180418.alamanc.interface.unique.php b/resources/sql/autopatches/20180418.alamanc.interface.unique.php
new file mode 100644
index 000000000..0ad4fbea1
--- /dev/null
+++ b/resources/sql/autopatches/20180418.alamanc.interface.unique.php
@@ -0,0 +1,85 @@
+<?php
+
+$interface_table = new AlmanacInterface();
+$binding_table = new AlmanacBinding();
+$interface_conn = $interface_table->establishConnection('w');
+
+queryfx(
+ $interface_conn,
+ 'LOCK TABLES %T WRITE, %T WRITE',
+ $interface_table->getTableName(),
+ $binding_table->getTableName());
+
+$seen = array();
+foreach (new LiskMigrationIterator($interface_table) as $interface) {
+ $device = $interface->getDevicePHID();
+ $network = $interface->getNetworkPHID();
+ $address = $interface->getAddress();
+ $port = $interface->getPort();
+ $key = "{$device}/{$network}/{$address}/{$port}";
+
+ // If this is the first copy of this row we've seen, mark it as seen and
+ // move on.
+ if (empty($seen[$key])) {
+ $seen[$key] = $interface->getID();
+ continue;
+ }
+
+ $survivor = queryfx_one(
+ $interface_conn,
+ 'SELECT * FROM %T WHERE id = %d',
+ $interface_table->getTableName(),
+ $seen[$key]);
+
+ $bindings = queryfx_all(
+ $interface_conn,
+ 'SELECT * FROM %T WHERE interfacePHID = %s',
+ $binding_table->getTableName(),
+ $interface->getPHID());
+
+ // Repoint bindings to the survivor.
+ foreach ($bindings as $binding) {
+ // Check if there's already a binding to the survivor.
+ $existing = queryfx_one(
+ $interface_conn,
+ 'SELECT * FROM %T WHERE interfacePHID = %s and devicePHID = %s and '.
+ 'servicePHID = %s',
+ $binding_table->getTableName(),
+ $survivor['phid'],
+ $binding['devicePHID'],
+ $binding['servicePHID']);
+
+ if (!$existing) {
+ // Reattach this binding to the survivor.
+ queryfx(
+ $interface_conn,
+ 'UPDATE %T SET interfacePHID = %s WHERE id = %d',
+ $binding_table->getTableName(),
+ $survivor['phid'],
+ $binding['id']);
+ } else {
+ // Binding to survivor already exists. Remove this now-redundant binding.
+ queryfx(
+ $interface_conn,
+ 'DELETE FROM %T WHERE id = %d',
+ $binding_table->getTableName(),
+ $binding['id']);
+ }
+ }
+
+ queryfx(
+ $interface_conn,
+ 'DELETE FROM %T WHERE id = %d',
+ $interface_table->getTableName(),
+ $interface->getID());
+}
+
+queryfx(
+ $interface_conn,
+ 'ALTER TABLE %T ADD UNIQUE KEY `key_unique` '.
+ '(devicePHID, networkPHID, address, port)',
+ $interface_table->getTableName());
+
+queryfx(
+ $interface_conn,
+ 'UNLOCK TABLES');
diff --git a/resources/sql/autopatches/20180418.almanac.network.unique.php b/resources/sql/autopatches/20180418.almanac.network.unique.php
new file mode 100644
index 000000000..c81c59823
--- /dev/null
+++ b/resources/sql/autopatches/20180418.almanac.network.unique.php
@@ -0,0 +1,46 @@
+<?php
+
+$table = new AlmanacNetwork();
+$conn = $table->establishConnection('w');
+
+queryfx(
+ $conn,
+ 'LOCK TABLES %T WRITE',
+ $table->getTableName());
+
+$seen = array();
+foreach (new LiskMigrationIterator($table) as $network) {
+ $name = $network->getName();
+
+ // If this is the first copy of this row we've seen, mark it as seen and
+ // move on.
+ if (empty($seen[$name])) {
+ $seen[$name] = 1;
+ continue;
+ }
+
+ // Otherwise, rename this row.
+ while (true) {
+ $new_name = $name.'-'.$seen[$name];
+ if (empty($seen[$new_name])) {
+ $network->setName($new_name);
+ try {
+ $network->save();
+ break;
+ } catch (AphrontDuplicateKeyQueryException $ex) {
+ // New name is a dupe of a network we haven't seen yet.
+ }
+ }
+ $seen[$name]++;
+ }
+ $seen[$new_name] = 1;
+}
+
+queryfx(
+ $conn,
+ 'ALTER TABLE %T ADD UNIQUE KEY `key_name` (name)',
+ $table->getTableName());
+
+queryfx(
+ $conn,
+ 'UNLOCK TABLES');
diff --git a/resources/sql/autopatches/20180419.phlux.edges.sql b/resources/sql/autopatches/20180419.phlux.edges.sql
new file mode 100644
index 000000000..1a63aa4d1
--- /dev/null
+++ b/resources/sql/autopatches/20180419.phlux.edges.sql
@@ -0,0 +1,16 @@
+CREATE TABLE {$NAMESPACE}_phlux.edge (
+ src VARBINARY(64) NOT NULL,
+ type INT UNSIGNED NOT NULL,
+ dst VARBINARY(64) NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ seq INT UNSIGNED NOT NULL,
+ dataID INT UNSIGNED,
+ PRIMARY KEY (src, type, dst),
+ KEY `src` (src, type, dateCreated, seq),
+ UNIQUE KEY `key_dst` (dst, type, src)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_phlux.edgedata (
+ id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180423.mail.01.properties.sql b/resources/sql/autopatches/20180423.mail.01.properties.sql
new file mode 100644
index 000000000..d4fc00802
--- /dev/null
+++ b/resources/sql/autopatches/20180423.mail.01.properties.sql
@@ -0,0 +1,8 @@
+CREATE TABLE {$NAMESPACE}_metamta.metamta_mailproperties (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ objectPHID VARBINARY(64) NOT NULL,
+ mailProperties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_object` (objectPHID)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180430.repo_identity.sql b/resources/sql/autopatches/20180430.repo_identity.sql
new file mode 100644
index 000000000..1d81d5c97
--- /dev/null
+++ b/resources/sql/autopatches/20180430.repo_identity.sql
@@ -0,0 +1,14 @@
+CREATE TABLE {$NAMESPACE}_repository.repository_identity (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ automaticGuessedUserPHID VARBINARY(64) DEFAULT NULL,
+ manuallySetUserPHID VARBINARY(64) DEFAULT NULL,
+ currentEffectiveUserPHID VARBINARY(64) DEFAULT NULL,
+ identityNameHash BINARY(12) NOT NULL,
+ identityNameRaw LONGBLOB NOT NULL,
+ identityNameEncoding VARCHAR(16) DEFAULT NULL COLLATE {$COLLATE_TEXT},
+ UNIQUE KEY `key_phid` (phid),
+ UNIQUE KEY `key_identity` (identityNameHash)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180504.owners.01.mailkey.php b/resources/sql/autopatches/20180504.owners.01.mailkey.php
new file mode 100644
index 000000000..c1b5550f9
--- /dev/null
+++ b/resources/sql/autopatches/20180504.owners.01.mailkey.php
@@ -0,0 +1,26 @@
+<?php
+
+$packages_table = new PhabricatorOwnersPackage();
+$packages_conn = $packages_table->establishConnection('w');
+$packages_name = $packages_table->getTableName();
+
+$properties_table = new PhabricatorMetaMTAMailProperties();
+$conn = $properties_table->establishConnection('w');
+
+$iterator = new LiskRawMigrationIterator($packages_conn, $packages_name);
+foreach ($iterator as $package) {
+ queryfx(
+ $conn,
+ 'INSERT IGNORE INTO %T
+ (objectPHID, mailProperties, dateCreated, dateModified)
+ VALUES
+ (%s, %s, %d, %d)',
+ $properties_table->getTableName(),
+ $package['phid'],
+ phutil_json_encode(
+ array(
+ 'mailKey' => $package['mailKey'],
+ )),
+ PhabricatorTime::getNow(),
+ PhabricatorTime::getNow());
+}
diff --git a/resources/sql/autopatches/20180504.owners.02.rmkey.sql b/resources/sql/autopatches/20180504.owners.02.rmkey.sql
new file mode 100644
index 000000000..5b8f24030
--- /dev/null
+++ b/resources/sql/autopatches/20180504.owners.02.rmkey.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_package
+ DROP mailKey;
diff --git a/resources/sql/autopatches/20180504.owners.03.properties.sql b/resources/sql/autopatches/20180504.owners.03.properties.sql
new file mode 100644
index 000000000..d7a90ed1c
--- /dev/null
+++ b/resources/sql/autopatches/20180504.owners.03.properties.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_package
+ ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180504.owners.04.default.sql b/resources/sql/autopatches/20180504.owners.04.default.sql
new file mode 100644
index 000000000..c4c7ff044
--- /dev/null
+++ b/resources/sql/autopatches/20180504.owners.04.default.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_owners.owners_package
+ SET properties = '{}' WHERE properties = '';
diff --git a/resources/sql/autopatches/20180504.repo_identity.author.sql b/resources/sql/autopatches/20180504.repo_identity.author.sql
new file mode 100644
index 000000000..95859a620
--- /dev/null
+++ b/resources/sql/autopatches/20180504.repo_identity.author.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_repository.repository_identity
+ ADD COLUMN authorPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20180504.repo_identity.xaction.sql b/resources/sql/autopatches/20180504.repo_identity.xaction.sql
new file mode 100644
index 000000000..4b4e1f2a2
--- /dev/null
+++ b/resources/sql/autopatches/20180504.repo_identity.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_repository.repository_identitytransaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ authorPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentPHID VARBINARY(64) DEFAULT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20180509.repo_identity.commits.sql b/resources/sql/autopatches/20180509.repo_identity.commits.sql
new file mode 100644
index 000000000..cc3ed299b
--- /dev/null
+++ b/resources/sql/autopatches/20180509.repo_identity.commits.sql
@@ -0,0 +1,3 @@
+ALTER TABLE {$NAMESPACE}_repository.repository_commit
+ ADD COLUMN authorIdentityPHID VARBINARY(64) DEFAULT NULL,
+ ADD COLUMN committerIdentityPHID VARBINARY(64) DEFAULT NULL;
diff --git a/resources/sql/autopatches/20180730.phriction.01.spaces.sql b/resources/sql/autopatches/20180730.phriction.01.spaces.sql
new file mode 100644
index 000000000..6d3e00725
--- /dev/null
+++ b/resources/sql/autopatches/20180730.phriction.01.spaces.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ ADD spacePHID VARBINARY(64) DEFAULT NULL;
diff --git a/resources/sql/autopatches/20180730.project.01.spaces.sql b/resources/sql/autopatches/20180730.project.01.spaces.sql
new file mode 100644
index 000000000..927ff3b67
--- /dev/null
+++ b/resources/sql/autopatches/20180730.project.01.spaces.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_project.project
+ ADD COLUMN spacePHID VARBINARY(64) DEFAULT NULL;
diff --git a/resources/sql/autopatches/20180809.repo_identities.activity.php b/resources/sql/autopatches/20180809.repo_identities.activity.php
new file mode 100644
index 000000000..e1077d4dd
--- /dev/null
+++ b/resources/sql/autopatches/20180809.repo_identities.activity.php
@@ -0,0 +1,20 @@
+<?php
+
+// Advise installs to rebuild the repository identities.
+
+// If the install has no commits (or no commits that lack an
+// authorIdentityPHID), don't require a rebuild.
+$commits = id(new PhabricatorRepositoryCommit())
+ ->loadAllWhere('authorIdentityPHID IS NULL LIMIT 1');
+
+if (!$commits) {
+ return;
+}
+
+try {
+ id(new PhabricatorConfigManualActivity())
+ ->setActivityType(PhabricatorConfigManualActivity::TYPE_IDENTITIES)
+ ->save();
+} catch (AphrontDuplicateKeyQueryException $ex) {
+ // If we've already noted that this activity is required, just move on.
+}
diff --git a/resources/sql/autopatches/20180827.drydock.01.acquired.sql b/resources/sql/autopatches/20180827.drydock.01.acquired.sql
new file mode 100644
index 000000000..55948391c
--- /dev/null
+++ b/resources/sql/autopatches/20180827.drydock.01.acquired.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_drydock.drydock_lease
+ ADD acquiredEpoch INT UNSIGNED;
diff --git a/resources/sql/autopatches/20180827.drydock.02.activated.sql b/resources/sql/autopatches/20180827.drydock.02.activated.sql
new file mode 100644
index 000000000..552f7b6b2
--- /dev/null
+++ b/resources/sql/autopatches/20180827.drydock.02.activated.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_drydock.drydock_lease
+ ADD activatedEpoch INT UNSIGNED;
diff --git a/resources/sql/autopatches/20180828.phriction.01.contentphid.sql b/resources/sql/autopatches/20180828.phriction.01.contentphid.sql
new file mode 100644
index 000000000..cf3d78ebd
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.01.contentphid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ ADD contentPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20180828.phriction.02.documentphid.sql b/resources/sql/autopatches/20180828.phriction.02.documentphid.sql
new file mode 100644
index 000000000..c15b4b17b
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.02.documentphid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_content
+ ADD documentPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20180828.phriction.03.editedepoch.sql b/resources/sql/autopatches/20180828.phriction.03.editedepoch.sql
new file mode 100644
index 000000000..eae31fc0b
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.03.editedepoch.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ ADD editedEpoch INT UNSIGNED NOT NULL;
diff --git a/resources/sql/autopatches/20180828.phriction.04.migrate.php b/resources/sql/autopatches/20180828.phriction.04.migrate.php
new file mode 100644
index 000000000..461eae2da
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.04.migrate.php
@@ -0,0 +1,57 @@
+<?php
+
+// Update the PhrictionDocument and PhrictionContent tables to refer to one
+// another by PHID instead of by ID.
+
+$document_table = new PhrictionDocument();
+$content_table = new PhrictionContent();
+
+$conn = $document_table->establishConnection('w');
+
+$document_iterator = new LiskRawMigrationIterator(
+ $conn,
+ $document_table->getTableName());
+foreach ($document_iterator as $row) {
+ $content_id = $row['contentID'];
+
+ $content_row = queryfx_one(
+ $conn,
+ 'SELECT phid, dateCreated FROM %T WHERE id = %d',
+ $content_table->getTableName(),
+ $content_id);
+
+ if (!$content_row) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET contentPHID = %s, editedEpoch = %d WHERE id = %d',
+ $document_table->getTableName(),
+ $content_row['phid'],
+ $content_row['dateCreated'],
+ $row['id']);
+}
+
+$content_iterator = new LiskRawMigrationIterator(
+ $conn,
+ $content_table->getTableName());
+foreach ($content_iterator as $row) {
+ $document_id = $row['documentID'];
+
+ $document_row = queryfx_one(
+ $conn,
+ 'SELECT phid FROM %T WHERE id = %d',
+ $document_table->getTableName(),
+ $document_id);
+ if (!$document_row) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET documentPHID = %s WHERE id = %d',
+ $content_table->getTableName(),
+ $document_row['phid'],
+ $row['id']);
+}
diff --git a/resources/sql/autopatches/20180828.phriction.05.contentid.sql b/resources/sql/autopatches/20180828.phriction.05.contentid.sql
new file mode 100644
index 000000000..d6cba741a
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.05.contentid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ DROP contentID;
diff --git a/resources/sql/autopatches/20180828.phriction.06.c.documentid.php b/resources/sql/autopatches/20180828.phriction.06.c.documentid.php
new file mode 100644
index 000000000..474643d62
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.06.c.documentid.php
@@ -0,0 +1,20 @@
+<?php
+
+// See T13193. We're about to drop the "documentID" column, which is part of
+// a UNIQUE KEY. In MariaDB, we must first drop the "documentID" key or we get
+// into deep trouble.
+
+// There's no "IF EXISTS" modifier for "ALTER TABLE" so run this as a PHP patch
+// instead of an SQL patch.
+
+$table = new PhrictionContent();
+$conn = $table->establishConnection('w');
+
+try {
+ queryfx(
+ $conn,
+ 'ALTER TABLE %T DROP KEY documentID',
+ $table->getTableName());
+} catch (AphrontQueryException $ex) {
+ // Ignore.
+}
diff --git a/resources/sql/autopatches/20180828.phriction.06.documentid.sql b/resources/sql/autopatches/20180828.phriction.06.documentid.sql
new file mode 100644
index 000000000..2323154b3
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.06.documentid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_content
+ DROP documentID;
diff --git a/resources/sql/autopatches/20180828.phriction.07.c.documentuniq.sql b/resources/sql/autopatches/20180828.phriction.07.c.documentuniq.sql
new file mode 100644
index 000000000..d086cc614
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.07.c.documentuniq.sql
@@ -0,0 +1 @@
+DELETE FROM {$NAMESPACE}_phriction.phriction_content WHERE documentPHID = '';
diff --git a/resources/sql/autopatches/20180828.phriction.07.documentkey.sql b/resources/sql/autopatches/20180828.phriction.07.documentkey.sql
new file mode 100644
index 000000000..aea3c9713
--- /dev/null
+++ b/resources/sql/autopatches/20180828.phriction.07.documentkey.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_content
+ ADD UNIQUE KEY `key_version` (documentPHID, version);
diff --git a/resources/sql/autopatches/20180829.phriction.01.mailkey.php b/resources/sql/autopatches/20180829.phriction.01.mailkey.php
new file mode 100644
index 000000000..cb85a3c5e
--- /dev/null
+++ b/resources/sql/autopatches/20180829.phriction.01.mailkey.php
@@ -0,0 +1,26 @@
+<?php
+
+$document_table = new PhrictionDocument();
+$document_conn = $document_table->establishConnection('w');
+$document_name = $document_table->getTableName();
+
+$properties_table = new PhabricatorMetaMTAMailProperties();
+$conn = $properties_table->establishConnection('w');
+
+$iterator = new LiskRawMigrationIterator($document_conn, $document_name);
+foreach ($iterator as $row) {
+ queryfx(
+ $conn,
+ 'INSERT IGNORE INTO %T
+ (objectPHID, mailProperties, dateCreated, dateModified)
+ VALUES
+ (%s, %s, %d, %d)',
+ $properties_table->getTableName(),
+ $row['phid'],
+ phutil_json_encode(
+ array(
+ 'mailKey' => $row['mailKey'],
+ )),
+ PhabricatorTime::getNow(),
+ PhabricatorTime::getNow());
+}
diff --git a/resources/sql/autopatches/20180829.phriction.02.rmkey.sql b/resources/sql/autopatches/20180829.phriction.02.rmkey.sql
new file mode 100644
index 000000000..8199287db
--- /dev/null
+++ b/resources/sql/autopatches/20180829.phriction.02.rmkey.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ DROP mailKey;
diff --git a/resources/sql/autopatches/20180830.phriction.01.maxversion.sql b/resources/sql/autopatches/20180830.phriction.01.maxversion.sql
new file mode 100644
index 000000000..f6f24e833
--- /dev/null
+++ b/resources/sql/autopatches/20180830.phriction.01.maxversion.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phriction.phriction_document
+ ADD maxVersion INT UNSIGNED NOT NULL;
diff --git a/resources/sql/autopatches/20180830.phriction.02.maxes.php b/resources/sql/autopatches/20180830.phriction.02.maxes.php
new file mode 100644
index 000000000..97abf010d
--- /dev/null
+++ b/resources/sql/autopatches/20180830.phriction.02.maxes.php
@@ -0,0 +1,30 @@
+<?php
+
+// Populate the "maxVersion" column by copying the maximum "version" from the
+// content table.
+
+$document_table = new PhrictionDocument();
+$content_table = new PhrictionContent();
+
+$conn = $document_table->establishConnection('w');
+
+$iterator = new LiskRawMigrationIterator(
+ $conn,
+ $document_table->getTableName());
+foreach ($iterator as $row) {
+ $content = queryfx_one(
+ $conn,
+ 'SELECT MAX(version) max FROM %T WHERE documentPHID = %s',
+ $content_table->getTableName(),
+ $row['phid']);
+ if (!$content) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %T SET maxVersion = %d WHERE id = %d',
+ $document_table->getTableName(),
+ $content['max'],
+ $row['id']);
+}
diff --git a/scripts/install/install_ubuntu.sh b/scripts/install/install_ubuntu.sh
index 5408cdb3e..ab83c3a46 100755
--- a/scripts/install/install_ubuntu.sh
+++ b/scripts/install/install_ubuntu.sh
@@ -1,92 +1,123 @@
#!/bin/bash
confirm() {
echo "Press RETURN to continue, or ^C to cancel.";
read -e ignored
}
-GIT='git'
+INSTALL_URI=" https://phurl.io/u/install"
+
+failed() {
+ echo
+ echo
+ echo "Installation has failed."
+ echo "Text above this message might be useful to understanding what exactly failed."
+ echo
+ echo "Please follow this guide to manually complete installation:"
+ echo
+ echo $INSTALL_URI
+ echo
+ echo "We apologize for the inconvenience."
+ exit 3
+}
-LTS="Ubuntu 10.04"
ISSUE=`cat /etc/issue`
if [[ $ISSUE != Ubuntu* ]]
then
echo "This script is intended for use on Ubuntu, but this system appears";
echo "to be something else. Your results may vary.";
echo
confirm
-elif [[ `expr match "$ISSUE" "$LTS"` -eq ${#LTS} ]]
-then
- GIT='git-core'
fi
echo "PHABRICATOR UBUNTU INSTALL SCRIPT";
-echo "This script will install Phabricator and all of its core dependencies.";
+echo "This script will install Apache, Phabricator and its core dependencies.";
echo "Run it from the directory you want to install into.";
echo
-ROOT=`pwd`
-echo "Phabricator will be installed to: ${ROOT}.";
-confirm
-
echo "Testing sudo..."
sudo true
if [ $? -ne 0 ]
then
echo "ERROR: You must be able to sudo to run this script.";
exit 1;
fi;
-echo "Installing dependencies: git, apache, mysql, php...";
-echo
+echo 'Testing Ubuntu version...'
-set +x
+VERSION=`lsb_release -rs`
+MAJOR=`expr match "$VERSION" '\([0-9]*\)'`
-sudo apt-get -qq update
-sudo apt-get install \
- $GIT mysql-server apache2 dpkg-dev \
- php5 php5-mysqlnd php5-gd php5-dev php5-curl php-apc php5-cli php5-json
-
-# Enable mod_rewrite
-sudo a2enmod rewrite
+if [ "$MAJOR" -lt 16 ]
+then
+ echo 'This script is intented to install on modern operating systems; Your '
+ echo 'operating system is too old for this script.'
+ echo 'You can still install Phabricator manually - please consult the installation'
+ echo 'guide to see how:'
+ echo
+ echo $INSTALL_URI
+ echo
+ exit 2
+fi
-HAVEPCNTL=`php -r "echo extension_loaded('pcntl');"`
-if [ $HAVEPCNTL != "1" ]
+# Ubuntu 16.04 LTS only has php 7.0 in their repos, so they need this extra ppa.
+# Ubuntu 17.4 and up have official 7.2 builds.
+if [ "$MAJOR" -eq 16 ]
then
- echo "Installing pcntl...";
+ echo 'This version of Ubuntu requires additional resources in order to install'
+ echo 'and run Phabricator.'
+ echo 'We will now add a the following package repository to your system:'
+ echo ' https://launchpad.net/~ondrej/+archive/ubuntu/php'
echo
- apt-get source php5
- PHP5=`ls -1F | grep '^php5-.*/$'`
- (cd $PHP5/ext/pcntl && phpize && ./configure && make && sudo make install)
-else
- echo "pcntl already installed";
+ echo 'This repository is generally considered safe to use.'
+ confirm
+
+ sudo add-apt-repository -y ppa:ondrej/php || failed
fi
+ROOT=`pwd`
+echo "Phabricator will be installed to: ${ROOT}.";
+confirm
+
+echo "Installing dependencies: git, apache, mysql, php...";
+echo
+sudo apt-get -qq update
+sudo apt-get install \
+ git mysql-server apache2 libapache2-mod-php \
+ php php-mysql php-gd php-curl php-apcu php-cli php-json php-mbstring \
+ || failed
+
+echo "Enabling mod_rewrite in Apache..."
+echo
+sudo a2enmod rewrite || failed
+
+echo "Downloading Phabricator and dependencies..."
+echo
if [ ! -e libphutil ]
then
git clone https://github.com/phacility/libphutil.git
else
(cd libphutil && git pull --rebase)
fi
if [ ! -e arcanist ]
then
git clone https://github.com/phacility/arcanist.git
else
(cd arcanist && git pull --rebase)
fi
if [ ! -e phabricator ]
then
git clone https://github.com/phacility/phabricator.git
else
(cd phabricator && git pull --rebase)
fi
echo
echo
echo "Install probably worked mostly correctly. Continue with the 'Configuration Guide':";
echo
echo " https://secure.phabricator.com/book/phabricator/article/configuration_guide/";
echo
-echo "You can delete any php5-* stuff that's left over in this directory if you want.";
+echo 'Next step is "Configuring Apache webserver".'
diff --git a/scripts/repository/rebuild_summaries.php b/scripts/repository/rebuild_summaries.php
deleted file mode 100755
index de3d5acca..000000000
--- a/scripts/repository/rebuild_summaries.php
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env php
-<?php
-
-$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
-
-$commit = new PhabricatorRepositoryCommit();
-
-$conn_w = id(new PhabricatorRepository())->establishConnection('w');
-$sizes = queryfx_all(
- $conn_w,
- 'SELECT repositoryID, count(*) N FROM %T GROUP BY repositoryID',
- $commit->getTableName());
-$sizes = ipull($sizes, 'N', 'repositoryID');
-
-$maxes = queryfx_all(
- $conn_w,
- 'SELECT repositoryID, max(epoch) maxEpoch FROM %T GROUP BY repositoryID',
- $commit->getTableName());
-$maxes = ipull($maxes, 'maxEpoch', 'repositoryID');
-
-
-$repository_ids = array_keys($sizes + $maxes);
-
-echo pht('Updating %d repositories', count($repository_ids));
-
-foreach ($repository_ids as $repository_id) {
- $last_commit = queryfx_one(
- $conn_w,
- 'SELECT id FROM %T WHERE repositoryID = %d AND epoch = %d LIMIT 1',
- $commit->getTableName(),
- $repository_id,
- idx($maxes, $repository_id, 0));
- if ($last_commit) {
- $last_commit = $last_commit['id'];
- } else {
- $last_commit = 0;
- }
- queryfx(
- $conn_w,
- 'INSERT INTO %T (repositoryID, lastCommitID, size, epoch)
- VALUES (%d, %d, %d, %d) ON DUPLICATE KEY UPDATE
- lastCommitID = VALUES(lastCommitID),
- size = VALUES(size),
- epoch = VALUES(epoch)',
- PhabricatorRepository::TABLE_SUMMARY,
- $repository_id,
- $last_commit,
- idx($sizes, $repository_id, 0),
- idx($maxes, $repository_id, 0));
- echo '.';
-}
-echo "\n".pht('Done.')."\n";
diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php
index 53165dc3d..631098b16 100755
--- a/scripts/ssh/ssh-auth.php
+++ b/scripts/ssh/ssh-auth.php
@@ -1,69 +1,69 @@
#!/usr/bin/env php
<?php
-// C4science customized
+// C4science customized without cache
$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
+require_once $root.'/scripts/init/init-script.php';
if($_SERVER['argc'] < 4) {
exit(1);
}
$argv = $_SERVER['argv'];
$key = $argv[2];
$type = $argv[3];
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($type . ' ' . $key);
$ssh_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withKeys(array($public_key))
->withIsActive(true)
->executeOne();
if($ssh_key) {
$object = $ssh_key->getObject();
if (!($object instanceof PhabricatorUser)) {
exit(1);
}
$bin = $root.'/bin/ssh-exec';
$key_argv = array();
$key_argv[] = '--phabricator-ssh-user';
$key_argv[] = $object->getUsername();
$key_argv[] = '--phabricator-ssh-key';
$key_argv[] = $ssh_key->getID();
$cmd = csprintf('%s %Ls', $bin, $key_argv);
$cmd = addcslashes($cmd, '"\\');
// Strip out newlines and other nonsense from the key type and key body.
$type = $ssh_key->getKeyType();
$type = preg_replace('@[\x00-\x20]+@', '', $type);
if (!strlen($type)) {
exit(1);
}
$key = $ssh_key->getKeyBody();
$key = preg_replace('@[\x00-\x20]+@', '', $key);
if (!strlen($key)) {
exit(1);
}
$options = array(
'command="'.$cmd.'"',
'no-port-forwarding',
'no-X11-forwarding',
'no-agent-forwarding',
'no-pty',
);
$options = implode(',', $options);
echo $options.' '.$type.' '.$key."\n";
exit(0);
}
exit(1);
diff --git a/scripts/ssh/ssh-connect.php b/scripts/ssh/ssh-connect.php
index 9757f3cbb..0d87f1697 100755
--- a/scripts/ssh/ssh-connect.php
+++ b/scripts/ssh/ssh-connect.php
@@ -1,132 +1,159 @@
#!/usr/bin/env php
<?php
// This is a wrapper script for Git, Mercurial, and Subversion. It primarily
// serves to inject "-o StrictHostKeyChecking=no" into the SSH arguments.
// In some cases, Subversion sends us SIGTERM. If we don't catch the signal and
// react to it, we won't run object destructors by default and thus won't clean
// up temporary files. Declare ticks so we can install a signal handler.
if (function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
} else {
declare(ticks = 1);
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
// Contrary to the documentation, Git may pass a "-p" flag. If it does, respect
// it and move it before the "--" argument.
$args = new PhutilArgumentParser($argv);
$args->parsePartial(
array(
array(
'name' => 'port',
'short' => 'p',
'param' => pht('port'),
'help' => pht('Port number to connect to.'),
),
+ array(
+ 'name' => 'options',
+ 'short' => 'o',
+ 'param' => pht('options'),
+ 'repeat' => true,
+ 'help' => pht('SSH options.'),
+ ),
));
+
$unconsumed_argv = $args->getUnconsumedArgumentVector();
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, 'ssh_connect_signal');
}
function ssh_connect_signal($signo) {
// This is just letting destructors fire. In particular, we want to clean
// up any temporary files we wrote. See T10547.
exit(128 + $signo);
}
$pattern = array();
$arguments = array();
$pattern[] = 'ssh';
$pattern[] = '-o';
$pattern[] = 'StrictHostKeyChecking=no';
// This prevents "known host" failures, and covers for issues where HOME is set
// to something unusual.
$pattern[] = '-o';
$pattern[] = 'UserKnownHostsFile=/dev/null';
$as_device = getenv('PHABRICATOR_AS_DEVICE');
$credential_phid = getenv('PHABRICATOR_CREDENTIAL');
if ($as_device) {
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
throw new Exception(
pht(
'Attempting to create an SSH connection that authenticates with '.
'the current device, but this host is not configured as a cluster '.
'device.'));
}
if ($credential_phid) {
throw new Exception(
pht(
'Attempting to proxy an SSH connection that authenticates with '.
'both the current device and a specific credential. These options '.
'are mutually exclusive.'));
}
}
if ($credential_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$key = PassphraseSSHKey::loadFromPHID($credential_phid, $viewer);
$pattern[] = '-l %P';
$arguments[] = $key->getUsernameEnvelope();
$pattern[] = '-i %P';
$arguments[] = $key->getKeyfileEnvelope();
}
if ($as_device) {
$pattern[] = '-l %R';
$arguments[] = AlmanacKeys::getClusterSSHUser();
$pattern[] = '-i %R';
$arguments[] = AlmanacKeys::getKeyPath('device.key');
}
// Subversion passes us a host in the form "domain.com:port", which is not
// valid for normal SSH but which we can parse into a valid "-p" flag.
$passthru_args = $unconsumed_argv;
$host = array_shift($passthru_args);
$parts = explode(':', $host, 2);
$host = $parts[0];
$port = $args->getArg('port');
if (!$port) {
if (count($parts) == 2) {
$port = $parts[1];
}
}
if ($port) {
$pattern[] = '-p %d';
$arguments[] = $port;
}
+$options = $args->getArg('options');
+$allowed_ssh_options = array('SendEnv=GIT_PROTOCOL');
+
+if (!empty($options)) {
+ foreach ($options as $option) {
+ if (array_search($option, $allowed_ssh_options) !== false) {
+ $pattern[] = '-o %s';
+ $arguments[] = $option;
+ } else {
+ throw new Exception(
+ pht(
+ 'Disallowed ssh option "%s" given with "-o". '.
+ 'Allowed options are: %s.',
+ $option,
+ implode(', ', $allowed_ssh_options)));
+ }
+ }
+}
+
$pattern[] = '--';
$pattern[] = '%s';
$arguments[] = $host;
foreach ($passthru_args as $passthru_arg) {
$pattern[] = '%s';
$arguments[] = $passthru_arg;
}
$pattern = implode(' ', $pattern);
array_unshift($arguments, $pattern);
$err = newv('PhutilExecPassthru', $arguments)
->execute();
exit($err);
diff --git a/scripts/symbols/generate_ctags_symbols.php b/scripts/symbols/generate_ctags_symbols.php
index 8d77bfc47..e93b0c5cb 100755
--- a/scripts/symbols/generate_ctags_symbols.php
+++ b/scripts/symbols/generate_ctags_symbols.php
@@ -1,134 +1,138 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setSynopsis(<<<EOSYNOPSIS
**generate_ctags_symbols.php** [__options__]
Generate repository symbols using Exuberant Ctags. Paths are read from stdin.
EOSYNOPSIS
);
$args->parseStandardArguments();
if (ctags_check_executable() == false) {
echo phutil_console_format(
"%s\n\n%s\n",
pht(
'Could not find Exuberant Ctags. Make sure it is installed and '.
'available in executable path.'),
pht(
'Exuberant Ctags project page: %s',
'http://ctags.sourceforge.net/'));
exit(1);
}
if (posix_isatty(STDIN)) {
echo phutil_console_format(
"%s\n",
pht(
'Usage: %s',
"find . -type f -name '*.py' | ./generate_ctags_symbols.php"));
exit(1);
}
$input = file_get_contents('php://stdin');
$data = array();
$futures = array();
foreach (explode("\n", trim($input)) as $file) {
+ if (!strlen($file)) {
+ continue;
+ }
+
$file = Filesystem::readablePath($file);
$futures[$file] = ctags_get_parser_future($file);
}
$futures = new FutureIterator($futures);
foreach ($futures->limit(8) as $file => $future) {
$tags = $future->resolve();
$tags = explode("\n", $tags[1]);
foreach ($tags as $tag) {
$parts = explode(';', $tag);
// Skip lines that we can not parse.
if (count($parts) < 2) {
continue;
}
// Split ctags information.
$tag_info = explode("\t", $parts[0]);
// Split exuberant ctags "extension fields" (additional information).
$parts[1] = trim($parts[1], "\t \"");
$extension_fields = explode("\t", $parts[1]);
// Skip lines that we can not parse.
if (count($tag_info) < 3 || count($extension_fields) < 2) {
continue;
}
// Default context to empty.
$extension_fields[] = '';
list($token, $file_path, $line_num) = $tag_info;
list($type, $language, $context) = $extension_fields;
// Skip lines with tokens containing a space.
if (strpos($token, ' ') !== false) {
continue;
}
// Strip "language:"
$language = substr($language, 9);
// To keep consistent with "Separate with commas, for example: php, py"
// in Arcanist Project edit form.
$language = str_ireplace('python', 'py', $language);
// Also, "normalize" C++ and C#.
$language = str_ireplace('c++', 'cpp', $language);
$language = str_ireplace('c#', 'cs', $language);
// Ruby has "singleton method", for example.
$type = substr(str_replace(' ', '_', $type), 0, 12);
// class:foo, struct:foo, union:foo, enum:foo, ...
$context = last(explode(':', $context, 2));
$ignore = array(
'variable' => true,
);
if (empty($ignore[$type])) {
print_symbol($file_path, $line_num, $type, $token, $context, $language);
}
}
}
function ctags_get_parser_future($path) {
$future = new ExecFuture('ctags -n --fields=Kls -o - %s', $path);
return $future;
}
function ctags_check_executable() {
$result = exec_manual('ctags --version');
return !empty($result[1]);
}
function print_symbol($file, $line_num, $type, $token, $context, $language) {
// Get rid of relative path.
$file = explode('/', $file);
if ($file[0] == '.' || $file[0] == '..') {
array_shift($file);
}
$file = '/'.implode('/', $file);
$parts = array(
$context,
$token,
$type,
strtolower($language),
$line_num,
$file,
);
echo implode(' ', $parts)."\n";
}
diff --git a/scripts/symbols/generate_php_symbols.php b/scripts/symbols/generate_php_symbols.php
index db8412764..af87d580d 100755
--- a/scripts/symbols/generate_php_symbols.php
+++ b/scripts/symbols/generate_php_symbols.php
@@ -1,123 +1,127 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setSynopsis(<<<EOSYNOPSIS
**generate_php_symbols.php** [__options__]
Generate repository symbols using XHPAST. Paths are read from stdin.
EOSYNOPSIS
);
$args->parseStandardArguments();
if (posix_isatty(STDIN)) {
echo phutil_console_format(
"%s\n",
pht(
'Usage: %s',
"find . -type f -name '*.php' | ./generate_php_symbols.php"));
exit(1);
}
$input = file_get_contents('php://stdin');
$data = array();
$futures = array();
foreach (explode("\n", trim($input)) as $file) {
+ if (!strlen($file)) {
+ continue;
+ }
+
$file = Filesystem::readablePath($file);
$data[$file] = Filesystem::readFile($file);
$futures[$file] = PhutilXHPASTBinary::getParserFuture($data[$file]);
}
$futures = new FutureIterator($futures);
foreach ($futures->limit(8) as $file => $future) {
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$data[$file],
$future->resolve());
$root = $tree->getRootNode();
$scopes = array();
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name = $function->getChildByIndex(2);
// Skip anonymous functions.
if (!$name->getConcreteString()) {
continue;
}
print_symbol($file, 'function', $name);
}
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1);
print_symbol($file, 'class', $class_name);
$scopes[] = array($class, $class_name);
}
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($interfaces as $interface) {
$interface_name = $interface->getChildByIndex(1);
// We don't differentiate classes and interfaces in highlighters.
print_symbol($file, 'class', $interface_name);
$scopes[] = array($interface, $interface_name);
}
$constants = $root->selectDescendantsOfType('n_CONSTANT_DECLARATION_LIST');
foreach ($constants as $constant_list) {
foreach ($constant_list->getChildren() as $constant) {
$constant_name = $constant->getChildByIndex(0);
print_symbol($file, 'constant', $constant_name);
}
}
foreach ($scopes as $scope) {
// This prints duplicate symbols in the case of nested classes.
// Luckily, PHP doesn't allow those.
list($class, $class_name) = $scope;
$consts = $class->selectDescendantsOfType(
'n_CLASS_CONSTANT_DECLARATION_LIST');
foreach ($consts as $const_list) {
foreach ($const_list->getChildren() as $const) {
$const_name = $const->getChildByIndex(0);
print_symbol($file, 'class_const', $const_name, $class_name);
}
}
$members = $class->selectDescendantsOfType(
'n_CLASS_MEMBER_DECLARATION_LIST');
foreach ($members as $member_list) {
foreach ($member_list->getChildren() as $member) {
if ($member->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') {
continue;
}
$member_name = $member->getChildByIndex(0);
print_symbol($file, 'member', $member_name, $class_name);
}
}
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$method_name = $method->getChildByIndex(2);
print_symbol($file, 'method', $method_name, $class_name);
}
}
}
function print_symbol($file, $type, XHPASTNode $node, $context = null) {
$parts = array(
$context ? $context->getConcreteString() : '',
// Variable tokens are `$name`, not just `name`, so strip the "$"" off of
// class field names
ltrim($node->getConcreteString(), '$'),
$type,
'php',
$node->getLineNumber(),
'/'.ltrim($file, './'),
);
echo implode(' ', $parts)."\n";
}
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index da5674cbb..c25019b1f 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,11291 +1,11410 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
*
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' => array(
'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
'AlmanacBindingDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php',
'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php',
'AlmanacBindingDisableTransaction' => 'applications/almanac/xaction/AlmanacBindingDisableTransaction.php',
'AlmanacBindingEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php',
'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php',
'AlmanacBindingEditEngine' => 'applications/almanac/editor/AlmanacBindingEditEngine.php',
'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php',
'AlmanacBindingInterfaceTransaction' => 'applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php',
'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php',
'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php',
'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php',
'AlmanacBindingSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingSearchConduitAPIMethod.php',
'AlmanacBindingSearchEngine' => 'applications/almanac/query/AlmanacBindingSearchEngine.php',
'AlmanacBindingServiceTransaction' => 'applications/almanac/xaction/AlmanacBindingServiceTransaction.php',
'AlmanacBindingSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php',
'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php',
'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php',
'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php',
'AlmanacBindingTransactionType' => 'applications/almanac/xaction/AlmanacBindingTransactionType.php',
'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php',
'AlmanacBindingsSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php',
'AlmanacCacheEngineExtension' => 'applications/almanac/engineextension/AlmanacCacheEngineExtension.php',
'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php',
'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php',
'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php',
'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php',
'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php',
'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php',
'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
'AlmanacDeletePropertyEditField' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditField.php',
'AlmanacDeletePropertyEditType' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditType.php',
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
'AlmanacDeviceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceDeletePropertyTransaction.php',
'AlmanacDeviceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceEditConduitAPIMethod.php',
'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php',
'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php',
'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php',
'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php',
'AlmanacDeviceNameTransaction' => 'applications/almanac/xaction/AlmanacDeviceNameTransaction.php',
'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php',
'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php',
'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php',
'AlmanacDeviceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php',
'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php',
'AlmanacDeviceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php',
'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php',
'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php',
'AlmanacDeviceTransactionType' => 'applications/almanac/xaction/AlmanacDeviceTransactionType.php',
'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php',
'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php',
'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php',
'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php',
'AlmanacInterfaceAddressTransaction' => 'applications/almanac/xaction/AlmanacInterfaceAddressTransaction.php',
'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php',
'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php',
'AlmanacInterfaceDestroyTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php',
'AlmanacInterfaceDeviceTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php',
'AlmanacInterfaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceEditConduitAPIMethod.php',
'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php',
'AlmanacInterfaceEditEngine' => 'applications/almanac/editor/AlmanacInterfaceEditEngine.php',
'AlmanacInterfaceEditor' => 'applications/almanac/editor/AlmanacInterfaceEditor.php',
'AlmanacInterfaceNetworkTransaction' => 'applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php',
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
'AlmanacInterfacePortTransaction' => 'applications/almanac/xaction/AlmanacInterfacePortTransaction.php',
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
'AlmanacInterfaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceSearchConduitAPIMethod.php',
'AlmanacInterfaceSearchEngine' => 'applications/almanac/query/AlmanacInterfaceSearchEngine.php',
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
'AlmanacInterfaceTransaction' => 'applications/almanac/storage/AlmanacInterfaceTransaction.php',
'AlmanacInterfaceTransactionType' => 'applications/almanac/xaction/AlmanacInterfaceTransactionType.php',
'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php',
'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
'AlmanacModularTransaction' => 'applications/almanac/storage/AlmanacModularTransaction.php',
'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php',
'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php',
'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php',
'AlmanacNamespaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceEditConduitAPIMethod.php',
'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php',
'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php',
'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php',
'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php',
'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php',
'AlmanacNamespaceNameTransaction' => 'applications/almanac/xaction/AlmanacNamespaceNameTransaction.php',
'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php',
'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php',
'AlmanacNamespaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceSearchConduitAPIMethod.php',
'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php',
'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php',
'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php',
'AlmanacNamespaceTransactionType' => 'applications/almanac/xaction/AlmanacNamespaceTransactionType.php',
'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php',
'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php',
'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php',
'AlmanacNetworkEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkEditConduitAPIMethod.php',
'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php',
'AlmanacNetworkEditEngine' => 'applications/almanac/editor/AlmanacNetworkEditEngine.php',
'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php',
'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php',
'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php',
'AlmanacNetworkNameTransaction' => 'applications/almanac/xaction/AlmanacNetworkNameTransaction.php',
'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php',
'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php',
'AlmanacNetworkSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkSearchConduitAPIMethod.php',
'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php',
'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php',
'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php',
'AlmanacNetworkTransactionType' => 'applications/almanac/xaction/AlmanacNetworkTransactionType.php',
'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.php',
'AlmanacPropertiesEditEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php',
'AlmanacPropertiesSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php',
'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php',
'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php',
'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php',
'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
'AlmanacPropertyEditEngine' => 'applications/almanac/editor/AlmanacPropertyEditEngine.php',
'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php',
'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php',
'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php',
'AlmanacService' => 'applications/almanac/storage/AlmanacService.php',
'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php',
'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php',
'AlmanacServiceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceDeletePropertyTransaction.php',
'AlmanacServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceEditConduitAPIMethod.php',
'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php',
'AlmanacServiceEditEngine' => 'applications/almanac/editor/AlmanacServiceEditEngine.php',
'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php',
'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php',
'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php',
'AlmanacServiceNameTransaction' => 'applications/almanac/xaction/AlmanacServiceNameTransaction.php',
'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php',
'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php',
'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php',
'AlmanacServiceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php',
'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php',
'AlmanacServiceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php',
'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php',
'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php',
'AlmanacServiceTransactionType' => 'applications/almanac/xaction/AlmanacServiceTransactionType.php',
'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php',
'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php',
'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php',
'AlmanacServiceTypeTransaction' => 'applications/almanac/xaction/AlmanacServiceTypeTransaction.php',
'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php',
'AlmanacSetPropertyEditField' => 'applications/almanac/engineextension/AlmanacSetPropertyEditField.php',
'AlmanacSetPropertyEditType' => 'applications/almanac/engineextension/AlmanacSetPropertyEditType.php',
'AlmanacTransactionType' => 'applications/almanac/xaction/AlmanacTransactionType.php',
'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php',
'Aphront304Response' => 'aphront/response/Aphront304Response.php',
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
'Aphront404Response' => 'aphront/response/Aphront404Response.php',
'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php',
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
'AphrontController' => 'aphront/AphrontController.php',
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php',
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
'AphrontDialogView' => 'view/AphrontDialogView.php',
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
'AphrontException' => 'aphront/exception/AphrontException.php',
'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php',
'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
'AphrontFormDateControlValue' => 'view/form/control/AphrontFormDateControlValue.php',
'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php',
'AphrontFormHandlesControl' => 'view/form/control/AphrontFormHandlesControl.php',
'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php',
'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php',
'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php',
'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php',
'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php',
'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php',
'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php',
'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php',
'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php',
'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php',
'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
'AphrontFormView' => 'view/form/AphrontFormView.php',
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php',
'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php',
'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
'AphrontJavelinView' => 'view/AphrontJavelinView.php',
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php',
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
'AphrontNullView' => 'view/AphrontNullView.php',
'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
'AphrontPageView' => 'view/page/AphrontPageView.php',
'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php',
'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
'AphrontRequest' => 'aphront/AphrontRequest.php',
'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php',
'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
'AphrontResponse' => 'aphront/response/AphrontResponse.php',
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
'AphrontSite' => 'aphront/site/AphrontSite.php',
'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
'AphrontStringHTTPParameterType' => 'aphront/httpparametertype/AphrontStringHTTPParameterType.php',
'AphrontStringListHTTPParameterType' => 'aphront/httpparametertype/AphrontStringListHTTPParameterType.php',
'AphrontTableView' => 'view/control/AphrontTableView.php',
'AphrontTagView' => 'view/AphrontTagView.php',
'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php',
'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php',
'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php',
'AphrontView' => 'view/AphrontView.php',
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php',
'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php',
'BulkPointsParameterType' => 'applications/transactions/bulk/type/BulkPointsParameterType.php',
'BulkRemarkupParameterType' => 'applications/transactions/bulk/type/BulkRemarkupParameterType.php',
'BulkSelectParameterType' => 'applications/transactions/bulk/type/BulkSelectParameterType.php',
'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php',
'BulkTokenizerParameterType' => 'applications/transactions/bulk/type/BulkTokenizerParameterType.php',
'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php',
'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php',
'CelerityAPI' => 'applications/celerity/CelerityAPI.php',
'CelerityDarkModePostprocessor' => 'applications/celerity/postprocessor/CelerityDarkModePostprocessor.php',
'CelerityDefaultPostprocessor' => 'applications/celerity/postprocessor/CelerityDefaultPostprocessor.php',
'CelerityHighContrastPostprocessor' => 'applications/celerity/postprocessor/CelerityHighContrastPostprocessor.php',
'CelerityLargeFontPostprocessor' => 'applications/celerity/postprocessor/CelerityLargeFontPostprocessor.php',
'CelerityManagementMapWorkflow' => 'applications/celerity/management/CelerityManagementMapWorkflow.php',
'CelerityManagementSyntaxWorkflow' => 'applications/celerity/management/CelerityManagementSyntaxWorkflow.php',
'CelerityManagementWorkflow' => 'applications/celerity/management/CelerityManagementWorkflow.php',
'CelerityPhabricatorResourceController' => 'applications/celerity/controller/CelerityPhabricatorResourceController.php',
'CelerityPhabricatorResources' => 'applications/celerity/resources/CelerityPhabricatorResources.php',
'CelerityPhysicalResources' => 'applications/celerity/resources/CelerityPhysicalResources.php',
'CelerityPhysicalResourcesTestCase' => 'applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php',
'CelerityPostprocessor' => 'applications/celerity/postprocessor/CelerityPostprocessor.php',
'CelerityPostprocessorTestCase' => 'applications/celerity/__tests__/CelerityPostprocessorTestCase.php',
'CelerityRedGreenPostprocessor' => 'applications/celerity/postprocessor/CelerityRedGreenPostprocessor.php',
'CelerityResourceController' => 'applications/celerity/controller/CelerityResourceController.php',
'CelerityResourceGraph' => 'applications/celerity/CelerityResourceGraph.php',
'CelerityResourceMap' => 'applications/celerity/CelerityResourceMap.php',
'CelerityResourceMapGenerator' => 'applications/celerity/CelerityResourceMapGenerator.php',
'CelerityResourceTransformer' => 'applications/celerity/CelerityResourceTransformer.php',
'CelerityResourceTransformerTestCase' => 'applications/celerity/__tests__/CelerityResourceTransformerTestCase.php',
'CelerityResources' => 'applications/celerity/resources/CelerityResources.php',
'CelerityResourcesOnDisk' => 'applications/celerity/resources/CelerityResourcesOnDisk.php',
'CeleritySpriteGenerator' => 'applications/celerity/CeleritySpriteGenerator.php',
'CelerityStaticResourceResponse' => 'applications/celerity/CelerityStaticResourceResponse.php',
'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php',
'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php',
'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php',
'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php',
'ConduitAPIMethodTestCase' => 'applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php',
'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php',
'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php',
'ConduitApplicationNotInstalledException' => 'applications/conduit/protocol/exception/ConduitApplicationNotInstalledException.php',
'ConduitBoolParameterType' => 'applications/conduit/parametertype/ConduitBoolParameterType.php',
'ConduitCall' => 'applications/conduit/call/ConduitCall.php',
'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php',
'ConduitColumnsParameterType' => 'applications/conduit/parametertype/ConduitColumnsParameterType.php',
'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php',
+ 'ConduitConstantDescription' => 'applications/conduit/data/ConduitConstantDescription.php',
'ConduitEpochParameterType' => 'applications/conduit/parametertype/ConduitEpochParameterType.php',
'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php',
'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php',
'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php',
'ConduitIntListParameterType' => 'applications/conduit/parametertype/ConduitIntListParameterType.php',
'ConduitIntParameterType' => 'applications/conduit/parametertype/ConduitIntParameterType.php',
'ConduitListParameterType' => 'applications/conduit/parametertype/ConduitListParameterType.php',
'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php',
'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php',
'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php',
'ConduitPHIDListParameterType' => 'applications/conduit/parametertype/ConduitPHIDListParameterType.php',
'ConduitPHIDParameterType' => 'applications/conduit/parametertype/ConduitPHIDParameterType.php',
'ConduitParameterType' => 'applications/conduit/parametertype/ConduitParameterType.php',
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
'ConduitPointsParameterType' => 'applications/conduit/parametertype/ConduitPointsParameterType.php',
'ConduitProjectListParameterType' => 'applications/conduit/parametertype/ConduitProjectListParameterType.php',
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php',
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
'ConduitStringListParameterType' => 'applications/conduit/parametertype/ConduitStringListParameterType.php',
'ConduitStringParameterType' => 'applications/conduit/parametertype/ConduitStringParameterType.php',
'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
'ConduitUserListParameterType' => 'applications/conduit/parametertype/ConduitUserListParameterType.php',
'ConduitUserParameterType' => 'applications/conduit/parametertype/ConduitUserParameterType.php',
'ConduitWildParameterType' => 'applications/conduit/parametertype/ConduitWildParameterType.php',
'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php',
'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php',
'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php',
'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php',
'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php',
'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php',
'ConpherenceEditConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceEditConduitAPIMethod.php',
'ConpherenceEditEngine' => 'applications/conpherence/editor/ConpherenceEditEngine.php',
'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php',
'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php',
'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php',
'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php',
'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
'ConpherenceParticipantController' => 'applications/conpherence/controller/ConpherenceParticipantController.php',
'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php',
'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php',
'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php',
'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php',
'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
'ConpherenceRoomEditController' => 'applications/conpherence/controller/ConpherenceRoomEditController.php',
'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php',
'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php',
'ConpherenceRoomPreferencesController' => 'applications/conpherence/controller/ConpherenceRoomPreferencesController.php',
'ConpherenceRoomSettings' => 'applications/conpherence/constants/ConpherenceRoomSettings.php',
'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php',
'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php',
'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php',
'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php',
'ConpherenceThreadDatasource' => 'applications/conpherence/typeahead/ConpherenceThreadDatasource.php',
'ConpherenceThreadDateMarkerTransaction' => 'applications/conpherence/xaction/ConpherenceThreadDateMarkerTransaction.php',
'ConpherenceThreadIndexEngineExtension' => 'applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php',
'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php',
'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php',
'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php',
'ConpherenceThreadParticipantsTransaction' => 'applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php',
'ConpherenceThreadPictureTransaction' => 'applications/conpherence/xaction/ConpherenceThreadPictureTransaction.php',
'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php',
'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php',
'ConpherenceThreadSearchController' => 'applications/conpherence/controller/ConpherenceThreadSearchController.php',
'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php',
'ConpherenceThreadTitleNgrams' => 'applications/conpherence/storage/ConpherenceThreadTitleNgrams.php',
'ConpherenceThreadTitleTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTitleTransaction.php',
'ConpherenceThreadTopicTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTopicTransaction.php',
'ConpherenceThreadTransactionType' => 'applications/conpherence/xaction/ConpherenceThreadTransactionType.php',
'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php',
'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php',
'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php',
'ConpherenceTransactionRenderer' => 'applications/conpherence/ConpherenceTransactionRenderer.php',
'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php',
'ConpherenceUpdateActions' => 'applications/conpherence/constants/ConpherenceUpdateActions.php',
'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php',
'ConpherenceUpdateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php',
'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php',
'CountdownEditConduitAPIMethod' => 'applications/countdown/conduit/CountdownEditConduitAPIMethod.php',
'CountdownSearchConduitAPIMethod' => 'applications/countdown/conduit/CountdownSearchConduitAPIMethod.php',
'DarkConsoleController' => 'applications/console/controller/DarkConsoleController.php',
'DarkConsoleCore' => 'applications/console/core/DarkConsoleCore.php',
'DarkConsoleDataController' => 'applications/console/controller/DarkConsoleDataController.php',
'DarkConsoleErrorLogPlugin' => 'applications/console/plugin/DarkConsoleErrorLogPlugin.php',
'DarkConsoleErrorLogPluginAPI' => 'applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php',
'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php',
'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php',
'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php',
'DarkConsoleRealtimePlugin' => 'applications/console/plugin/DarkConsoleRealtimePlugin.php',
'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php',
'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php',
'DarkConsoleStartupPlugin' => 'applications/console/plugin/DarkConsoleStartupPlugin.php',
'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php',
'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php',
'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php',
'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php',
'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php',
'DifferentialAuditorsCommitMessageField' => 'applications/differential/field/DifferentialAuditorsCommitMessageField.php',
'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php',
'DifferentialBlameRevisionCommitMessageField' => 'applications/differential/field/DifferentialBlameRevisionCommitMessageField.php',
'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php',
'DifferentialBlockingReviewerDatasource' => 'applications/differential/typeahead/DifferentialBlockingReviewerDatasource.php',
'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php',
'DifferentialBuildableEngine' => 'applications/differential/harbormaster/DifferentialBuildableEngine.php',
'DifferentialChangeDetailMailView' => 'applications/differential/mail/DifferentialChangeDetailMailView.php',
'DifferentialChangeHeraldFieldGroup' => 'applications/differential/herald/DifferentialChangeHeraldFieldGroup.php',
'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php',
'DifferentialChangesSinceLastUpdateField' => 'applications/differential/customfield/DifferentialChangesSinceLastUpdateField.php',
'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php',
'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php',
+ 'DifferentialChangesetEngine' => 'applications/differential/engine/DifferentialChangesetEngine.php',
'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php',
'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php',
'DifferentialChangesetListController' => 'applications/differential/controller/DifferentialChangesetListController.php',
'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php',
'DifferentialChangesetOneUpMailRenderer' => 'applications/differential/render/DifferentialChangesetOneUpMailRenderer.php',
'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php',
'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php',
'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php',
'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
'DifferentialChangesetSearchEngine' => 'applications/differential/query/DifferentialChangesetSearchEngine.php',
'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php',
'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php',
'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php',
'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php',
'DifferentialCommitMessageCustomField' => 'applications/differential/field/DifferentialCommitMessageCustomField.php',
'DifferentialCommitMessageField' => 'applications/differential/field/DifferentialCommitMessageField.php',
'DifferentialCommitMessageFieldTestCase' => 'applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php',
'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php',
'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php',
'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php',
+ 'DifferentialCommitsSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php',
'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php',
'DifferentialConflictsCommitMessageField' => 'applications/differential/field/DifferentialConflictsCommitMessageField.php',
'DifferentialController' => 'applications/differential/controller/DifferentialController.php',
'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php',
'DifferentialCreateCommentConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php',
'DifferentialCreateDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php',
'DifferentialCreateInlineConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php',
'DifferentialCreateMailReceiver' => 'applications/differential/mail/DifferentialCreateMailReceiver.php',
'DifferentialCreateRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php',
'DifferentialCreateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php',
'DifferentialCustomField' => 'applications/differential/customfield/DifferentialCustomField.php',
'DifferentialCustomFieldDependsOnParser' => 'applications/differential/parser/DifferentialCustomFieldDependsOnParser.php',
'DifferentialCustomFieldDependsOnParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldDependsOnParserTestCase.php',
'DifferentialCustomFieldNumericIndex' => 'applications/differential/storage/DifferentialCustomFieldNumericIndex.php',
'DifferentialCustomFieldRevertsParser' => 'applications/differential/parser/DifferentialCustomFieldRevertsParser.php',
'DifferentialCustomFieldRevertsParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldRevertsParserTestCase.php',
'DifferentialCustomFieldStorage' => 'applications/differential/storage/DifferentialCustomFieldStorage.php',
'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php',
'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php',
'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php',
'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php',
'DifferentialDiffAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialDiffAffectedFilesHeraldField.php',
'DifferentialDiffAuthorHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorHeraldField.php',
'DifferentialDiffAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorProjectsHeraldField.php',
'DifferentialDiffContentAddedHeraldField' => 'applications/differential/herald/DifferentialDiffContentAddedHeraldField.php',
'DifferentialDiffContentHeraldField' => 'applications/differential/herald/DifferentialDiffContentHeraldField.php',
'DifferentialDiffContentRemovedHeraldField' => 'applications/differential/herald/DifferentialDiffContentRemovedHeraldField.php',
'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php',
'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php',
'DifferentialDiffExtractionEngine' => 'applications/differential/engine/DifferentialDiffExtractionEngine.php',
'DifferentialDiffHeraldField' => 'applications/differential/herald/DifferentialDiffHeraldField.php',
'DifferentialDiffHeraldFieldGroup' => 'applications/differential/herald/DifferentialDiffHeraldFieldGroup.php',
'DifferentialDiffInlineCommentQuery' => 'applications/differential/query/DifferentialDiffInlineCommentQuery.php',
'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php',
'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php',
'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php',
'DifferentialDiffRepositoryHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryHeraldField.php',
'DifferentialDiffRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryProjectsHeraldField.php',
'DifferentialDiffSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialDiffSearchConduitAPIMethod.php',
'DifferentialDiffSearchEngine' => 'applications/differential/query/DifferentialDiffSearchEngine.php',
'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php',
'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php',
'DifferentialDiffTransactionQuery' => 'applications/differential/query/DifferentialDiffTransactionQuery.php',
'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php',
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php',
'DifferentialDraftField' => 'applications/differential/customfield/DifferentialDraftField.php',
'DifferentialExactUserFunctionDatasource' => 'applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php',
'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php',
'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php',
'DifferentialGetAllDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php',
'DifferentialGetCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php',
'DifferentialGetCommitPathsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php',
'DifferentialGetDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php',
'DifferentialGetRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRawDiffConduitAPIMethod.php',
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php',
'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
'DifferentialGitSVNIDCommitMessageField' => 'applications/differential/field/DifferentialGitSVNIDCommitMessageField.php',
'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php',
'DifferentialHeraldStateReasons' => 'applications/differential/herald/DifferentialHeraldStateReasons.php',
'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
'DifferentialHovercardEngineExtension' => 'applications/differential/engineextension/DifferentialHovercardEngineExtension.php',
'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php',
'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php',
'DifferentialHunkParserTestCase' => 'applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php',
'DifferentialHunkQuery' => 'applications/differential/query/DifferentialHunkQuery.php',
'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php',
'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php',
'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php',
'DifferentialInlineCommentMailView' => 'applications/differential/mail/DifferentialInlineCommentMailView.php',
'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
'DifferentialJIRAIssuesCommitMessageField' => 'applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php',
'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
'DifferentialLegacyQuery' => 'applications/differential/constants/DifferentialLegacyQuery.php',
'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php',
'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php',
'DifferentialMailEngineExtension' => 'applications/differential/engineextension/DifferentialMailEngineExtension.php',
'DifferentialMailView' => 'applications/differential/mail/DifferentialMailView.php',
'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php',
'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php',
'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php',
'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php',
'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php',
'DifferentialQueryConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryConduitAPIMethod.php',
'DifferentialQueryDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryDiffsConduitAPIMethod.php',
'DifferentialRawDiffRenderer' => 'applications/differential/render/DifferentialRawDiffRenderer.php',
'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php',
'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php',
'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php',
'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php',
'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php',
'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php',
'DifferentialResponsibleDatasource' => 'applications/differential/typeahead/DifferentialResponsibleDatasource.php',
'DifferentialResponsibleUserDatasource' => 'applications/differential/typeahead/DifferentialResponsibleUserDatasource.php',
'DifferentialResponsibleViewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialResponsibleViewerFunctionDatasource.php',
'DifferentialRevertPlanCommitMessageField' => 'applications/differential/field/DifferentialRevertPlanCommitMessageField.php',
'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
'DifferentialReviewedByCommitMessageField' => 'applications/differential/field/DifferentialReviewedByCommitMessageField.php',
'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php',
'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php',
'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php',
'DifferentialReviewersAddReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php',
'DifferentialReviewersAddSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php',
'DifferentialReviewersCommitMessageField' => 'applications/differential/field/DifferentialReviewersCommitMessageField.php',
'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php',
'DifferentialReviewersSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialReviewersSearchEngineAttachment.php',
'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php',
'DifferentialRevisionAbandonTransaction' => 'applications/differential/xaction/DifferentialRevisionAbandonTransaction.php',
'DifferentialRevisionAcceptTransaction' => 'applications/differential/xaction/DifferentialRevisionAcceptTransaction.php',
'DifferentialRevisionActionTransaction' => 'applications/differential/xaction/DifferentialRevisionActionTransaction.php',
'DifferentialRevisionAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialRevisionAffectedFilesHeraldField.php',
'DifferentialRevisionAuthorHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorHeraldField.php',
'DifferentialRevisionAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorProjectsHeraldField.php',
'DifferentialRevisionBuildableTransaction' => 'applications/differential/xaction/DifferentialRevisionBuildableTransaction.php',
'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php',
'DifferentialRevisionCloseTransaction' => 'applications/differential/xaction/DifferentialRevisionCloseTransaction.php',
'DifferentialRevisionClosedStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionClosedStatusDatasource.php',
'DifferentialRevisionCommandeerTransaction' => 'applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php',
'DifferentialRevisionContentAddedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentAddedHeraldField.php',
'DifferentialRevisionContentHeraldField' => 'applications/differential/herald/DifferentialRevisionContentHeraldField.php',
'DifferentialRevisionContentRemovedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentRemovedHeraldField.php',
'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php',
'DifferentialRevisionDependedOnByRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php',
'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php',
'DifferentialRevisionDraftEngine' => 'applications/differential/engine/DifferentialRevisionDraftEngine.php',
'DifferentialRevisionEditConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionEditConduitAPIMethod.php',
'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php',
'DifferentialRevisionEditEngine' => 'applications/differential/editor/DifferentialRevisionEditEngine.php',
'DifferentialRevisionFerretEngine' => 'applications/differential/search/DifferentialRevisionFerretEngine.php',
'DifferentialRevisionFulltextEngine' => 'applications/differential/search/DifferentialRevisionFulltextEngine.php',
'DifferentialRevisionGraph' => 'infrastructure/graph/DifferentialRevisionGraph.php',
'DifferentialRevisionHasChildRelationship' => 'applications/differential/relationships/DifferentialRevisionHasChildRelationship.php',
'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php',
'DifferentialRevisionHasCommitRelationship' => 'applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php',
'DifferentialRevisionHasParentRelationship' => 'applications/differential/relationships/DifferentialRevisionHasParentRelationship.php',
'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php',
'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php',
'DifferentialRevisionHasTaskRelationship' => 'applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php',
'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php',
'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php',
'DifferentialRevisionHoldDraftTransaction' => 'applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php',
'DifferentialRevisionIDCommitMessageField' => 'applications/differential/field/DifferentialRevisionIDCommitMessageField.php',
'DifferentialRevisionInlineTransaction' => 'applications/differential/xaction/DifferentialRevisionInlineTransaction.php',
'DifferentialRevisionInlinesController' => 'applications/differential/controller/DifferentialRevisionInlinesController.php',
'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
'DifferentialRevisionOpenStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionOpenStatusDatasource.php',
'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php',
'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php',
'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php',
'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php',
'DifferentialRevisionPlanChangesTransaction' => 'applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php',
'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php',
'DifferentialRevisionReclaimTransaction' => 'applications/differential/xaction/DifferentialRevisionReclaimTransaction.php',
'DifferentialRevisionRejectTransaction' => 'applications/differential/xaction/DifferentialRevisionRejectTransaction.php',
'DifferentialRevisionRelationship' => 'applications/differential/relationships/DifferentialRevisionRelationship.php',
'DifferentialRevisionRelationshipSource' => 'applications/search/relationship/DifferentialRevisionRelationshipSource.php',
'DifferentialRevisionReopenTransaction' => 'applications/differential/xaction/DifferentialRevisionReopenTransaction.php',
'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php',
'DifferentialRevisionRepositoryTransaction' => 'applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php',
'DifferentialRevisionRequestReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php',
'DifferentialRevisionRequiredActionResultBucket' => 'applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php',
'DifferentialRevisionResignTransaction' => 'applications/differential/xaction/DifferentialRevisionResignTransaction.php',
'DifferentialRevisionResultBucket' => 'applications/differential/query/DifferentialRevisionResultBucket.php',
'DifferentialRevisionReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewTransaction.php',
'DifferentialRevisionReviewersHeraldField' => 'applications/differential/herald/DifferentialRevisionReviewersHeraldField.php',
'DifferentialRevisionReviewersTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewersTransaction.php',
'DifferentialRevisionSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionSearchConduitAPIMethod.php',
'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php',
'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php',
'DifferentialRevisionStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusDatasource.php',
'DifferentialRevisionStatusFunctionDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusFunctionDatasource.php',
'DifferentialRevisionStatusHeraldField' => 'applications/differential/herald/DifferentialRevisionStatusHeraldField.php',
'DifferentialRevisionStatusTransaction' => 'applications/differential/xaction/DifferentialRevisionStatusTransaction.php',
'DifferentialRevisionSummaryHeraldField' => 'applications/differential/herald/DifferentialRevisionSummaryHeraldField.php',
'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php',
+ 'DifferentialRevisionTestPlanHeraldField' => 'applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php',
'DifferentialRevisionTestPlanTransaction' => 'applications/differential/xaction/DifferentialRevisionTestPlanTransaction.php',
'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php',
'DifferentialRevisionTitleTransaction' => 'applications/differential/xaction/DifferentialRevisionTitleTransaction.php',
'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php',
'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php',
'DifferentialRevisionUpdateTransaction' => 'applications/differential/xaction/DifferentialRevisionUpdateTransaction.php',
'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php',
'DifferentialRevisionWrongStateTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php',
'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
'DifferentialSubscribersCommitMessageField' => 'applications/differential/field/DifferentialSubscribersCommitMessageField.php',
'DifferentialSummaryCommitMessageField' => 'applications/differential/field/DifferentialSummaryCommitMessageField.php',
'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
'DifferentialTagsCommitMessageField' => 'applications/differential/field/DifferentialTagsCommitMessageField.php',
'DifferentialTasksCommitMessageField' => 'applications/differential/field/DifferentialTasksCommitMessageField.php',
'DifferentialTestPlanCommitMessageField' => 'applications/differential/field/DifferentialTestPlanCommitMessageField.php',
'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php',
'DifferentialTitleCommitMessageField' => 'applications/differential/field/DifferentialTitleCommitMessageField.php',
'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php',
'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php',
'DifferentialTransactionEditor' => 'applications/differential/editor/DifferentialTransactionEditor.php',
'DifferentialTransactionQuery' => 'applications/differential/query/DifferentialTransactionQuery.php',
'DifferentialTransactionView' => 'applications/differential/view/DifferentialTransactionView.php',
'DifferentialUnitField' => 'applications/differential/customfield/DifferentialUnitField.php',
'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php',
'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php',
'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php',
'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php',
'DiffusionAuditorFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorFunctionDatasource.php',
'DiffusionAuditorsAddAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php',
'DiffusionAuditorsAddSelfHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php',
'DiffusionAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsHeraldAction.php',
'DiffusionBlameConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBlameConduitAPIMethod.php',
'DiffusionBlameController' => 'applications/diffusion/controller/DiffusionBlameController.php',
'DiffusionBlameQuery' => 'applications/diffusion/query/blame/DiffusionBlameQuery.php',
'DiffusionBlockHeraldAction' => 'applications/diffusion/herald/DiffusionBlockHeraldAction.php',
'DiffusionBranchListView' => 'applications/diffusion/view/DiffusionBranchListView.php',
'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php',
'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php',
'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php',
'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php',
'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php',
'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php',
'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php',
'DiffusionBuildableEngine' => 'applications/diffusion/harbormaster/DiffusionBuildableEngine.php',
'DiffusionCacheEngineExtension' => 'applications/diffusion/engineextension/DiffusionCacheEngineExtension.php',
'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php',
'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php',
'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php',
'DiffusionCloneController' => 'applications/diffusion/controller/DiffusionCloneController.php',
'DiffusionCloneURIView' => 'applications/diffusion/view/DiffusionCloneURIView.php',
'DiffusionCommandEngine' => 'applications/diffusion/protocol/DiffusionCommandEngine.php',
'DiffusionCommandEngineTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionCommandEngineTestCase.php',
'DiffusionCommitAcceptTransaction' => 'applications/diffusion/xaction/DiffusionCommitAcceptTransaction.php',
'DiffusionCommitActionTransaction' => 'applications/diffusion/xaction/DiffusionCommitActionTransaction.php',
'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php',
+ 'DiffusionCommitAuditStatus' => 'applications/diffusion/DiffusionCommitAuditStatus.php',
'DiffusionCommitAuditTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditTransaction.php',
'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php',
'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php',
'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php',
'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php',
'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php',
'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php',
'DiffusionCommitBuildableTransaction' => 'applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php',
'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php',
'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php',
'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php',
'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php',
'DiffusionCommitDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentHeraldField.php',
'DiffusionCommitDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentRemovedHeraldField.php',
'DiffusionCommitDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffEnormousHeraldField.php',
'DiffusionCommitDraftEngine' => 'applications/diffusion/engine/DiffusionCommitDraftEngine.php',
'DiffusionCommitEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitEditConduitAPIMethod.php',
'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php',
'DiffusionCommitEditEngine' => 'applications/diffusion/editor/DiffusionCommitEditEngine.php',
'DiffusionCommitFerretEngine' => 'applications/repository/search/DiffusionCommitFerretEngine.php',
'DiffusionCommitFulltextEngine' => 'applications/repository/search/DiffusionCommitFulltextEngine.php',
'DiffusionCommitHasPackageEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasPackageEdgeType.php',
'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php',
'DiffusionCommitHasRevisionRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php',
'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php',
'DiffusionCommitHasTaskRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasTaskRelationship.php',
'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php',
'DiffusionCommitHeraldField' => 'applications/diffusion/herald/DiffusionCommitHeraldField.php',
'DiffusionCommitHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionCommitHeraldFieldGroup.php',
'DiffusionCommitHintQuery' => 'applications/diffusion/query/DiffusionCommitHintQuery.php',
'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php',
'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php',
'DiffusionCommitListController' => 'applications/diffusion/controller/DiffusionCommitListController.php',
'DiffusionCommitListView' => 'applications/diffusion/view/DiffusionCommitListView.php',
'DiffusionCommitMergeHeraldField' => 'applications/diffusion/herald/DiffusionCommitMergeHeraldField.php',
'DiffusionCommitMessageHeraldField' => 'applications/diffusion/herald/DiffusionCommitMessageHeraldField.php',
'DiffusionCommitPackageAuditHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php',
'DiffusionCommitPackageHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageHeraldField.php',
'DiffusionCommitPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageOwnerHeraldField.php',
'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php',
'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php',
'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php',
'DiffusionCommitRelationship' => 'applications/diffusion/relationships/DiffusionCommitRelationship.php',
'DiffusionCommitRelationshipSource' => 'applications/search/relationship/DiffusionCommitRelationshipSource.php',
'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php',
'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php',
'DiffusionCommitRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryHeraldField.php',
'DiffusionCommitRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryProjectsHeraldField.php',
'DiffusionCommitRequiredActionResultBucket' => 'applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php',
'DiffusionCommitResignTransaction' => 'applications/diffusion/xaction/DiffusionCommitResignTransaction.php',
'DiffusionCommitResultBucket' => 'applications/diffusion/query/DiffusionCommitResultBucket.php',
'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php',
'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php',
'DiffusionCommitReviewerHeraldField' => 'applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php',
'DiffusionCommitRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php',
'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptingReviewersHeraldField.php',
'DiffusionCommitRevisionHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionHeraldField.php',
'DiffusionCommitRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php',
'DiffusionCommitRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionSubscribersHeraldField.php',
'DiffusionCommitSearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitSearchConduitAPIMethod.php',
'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php',
'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php',
'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php',
'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php',
'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php',
'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php',
'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.php',
'DiffusionDaemonLockException' => 'applications/diffusion/exception/DiffusionDaemonLockException.php',
'DiffusionDatasourceEngineExtension' => 'applications/diffusion/engineextension/DiffusionDatasourceEngineExtension.php',
'DiffusionDefaultEditCapability' => 'applications/diffusion/capability/DiffusionDefaultEditCapability.php',
'DiffusionDefaultPushCapability' => 'applications/diffusion/capability/DiffusionDefaultPushCapability.php',
'DiffusionDefaultViewCapability' => 'applications/diffusion/capability/DiffusionDefaultViewCapability.php',
'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php',
'DiffusionDiffInlineCommentQuery' => 'applications/diffusion/query/DiffusionDiffInlineCommentQuery.php',
'DiffusionDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php',
'DiffusionDocumentController' => 'applications/diffusion/controller/DiffusionDocumentController.php',
'DiffusionDocumentRenderingEngine' => 'applications/diffusion/document/DiffusionDocumentRenderingEngine.php',
'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php',
'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php',
'DiffusionExistsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php',
'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php',
'DiffusionExternalSymbolQuery' => 'applications/diffusion/symbol/DiffusionExternalSymbolQuery.php',
'DiffusionExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionExternalSymbolsSource.php',
'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php',
'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php',
'DiffusionFileFutureQuery' => 'applications/diffusion/query/DiffusionFileFutureQuery.php',
'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php',
'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php',
'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php',
'DiffusionGitBlameQuery' => 'applications/diffusion/query/blame/DiffusionGitBlameQuery.php',
'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php',
'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
'DiffusionGitCommandEngine' => 'applications/diffusion/protocol/DiffusionGitCommandEngine.php',
'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php',
'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php',
'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php',
'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php',
'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php',
'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php',
'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php',
'DiffusionGraphController' => 'applications/diffusion/controller/DiffusionGraphController.php',
'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php',
'DiffusionHistoryListView' => 'applications/diffusion/view/DiffusionHistoryListView.php',
'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php',
'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php',
'DiffusionHistoryView' => 'applications/diffusion/view/DiffusionHistoryView.php',
'DiffusionHovercardEngineExtension' => 'applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php',
+ 'DiffusionIdentityAssigneeDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php',
+ 'DiffusionIdentityAssigneeEditField' => 'applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php',
+ 'DiffusionIdentityAssigneeSearchField' => 'applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php',
+ 'DiffusionIdentityEditController' => 'applications/diffusion/controller/DiffusionIdentityEditController.php',
+ 'DiffusionIdentityListController' => 'applications/diffusion/controller/DiffusionIdentityListController.php',
+ 'DiffusionIdentityUnassignedDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php',
+ 'DiffusionIdentityViewController' => 'applications/diffusion/controller/DiffusionIdentityViewController.php',
'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php',
'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php',
+ 'DiffusionInternalAncestorsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php',
'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php',
'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php',
'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php',
'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php',
'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php',
'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php',
'DiffusionLocalRepositoryFilter' => 'applications/diffusion/data/DiffusionLocalRepositoryFilter.php',
'DiffusionLogController' => 'applications/diffusion/controller/DiffusionLogController.php',
'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php',
'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php',
'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php',
'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php',
'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php',
'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php',
'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php',
'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php',
'DiffusionMercurialCommandEngine' => 'applications/diffusion/protocol/DiffusionMercurialCommandEngine.php',
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
'DiffusionMercurialFlagInjectionException' => 'applications/diffusion/exception/DiffusionMercurialFlagInjectionException.php',
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
'DiffusionMercurialSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialSSHWorkflow.php',
'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php',
'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
'DiffusionMercurialWireProtocolTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php',
'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php',
'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
'DiffusionPatternSearchView' => 'applications/diffusion/view/DiffusionPatternSearchView.php',
'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php',
'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php',
'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php',
'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php',
'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php',
'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentRemovedHeraldField.php',
'DiffusionPreCommitContentDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffEnormousHeraldField.php',
'DiffusionPreCommitContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentHeraldField.php',
'DiffusionPreCommitContentMergeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMergeHeraldField.php',
'DiffusionPreCommitContentMessageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMessageHeraldField.php',
'DiffusionPreCommitContentPackageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php',
'DiffusionPreCommitContentPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php',
'DiffusionPreCommitContentPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherHeraldField.php',
'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherIsCommitterHeraldField.php',
'DiffusionPreCommitContentPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php',
'DiffusionPreCommitContentRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryHeraldField.php',
'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryProjectsHeraldField.php',
'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php',
'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField.php',
'DiffusionPreCommitContentRevisionHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionHeraldField.php',
'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php',
'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionSubscribersHeraldField.php',
'DiffusionPreCommitRefChangeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefChangeHeraldField.php',
'DiffusionPreCommitRefHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldField.php',
'DiffusionPreCommitRefHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldFieldGroup.php',
'DiffusionPreCommitRefNameHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefNameHeraldField.php',
'DiffusionPreCommitRefPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherHeraldField.php',
'DiffusionPreCommitRefPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php',
'DiffusionPreCommitRefRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryHeraldField.php',
'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php',
'DiffusionPreCommitRefTypeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefTypeHeraldField.php',
'DiffusionPreCommitUsesGitLFSHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitUsesGitLFSHeraldField.php',
'DiffusionPullEventGarbageCollector' => 'applications/diffusion/garbagecollector/DiffusionPullEventGarbageCollector.php',
'DiffusionPullLogListController' => 'applications/diffusion/controller/DiffusionPullLogListController.php',
'DiffusionPullLogListView' => 'applications/diffusion/view/DiffusionPullLogListView.php',
'DiffusionPullLogSearchEngine' => 'applications/diffusion/query/DiffusionPullLogSearchEngine.php',
'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php',
'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php',
'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php',
'DiffusionPythonExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPythonExternalSymbolsSource.php',
'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php',
'DiffusionQueryCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php',
'DiffusionQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php',
'DiffusionQueryPathsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php',
'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php',
'DiffusionRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php',
'DiffusionReadmeView' => 'applications/diffusion/view/DiffusionReadmeView.php',
'DiffusionRefDatasource' => 'applications/diffusion/typeahead/DiffusionRefDatasource.php',
'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php',
'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
'DiffusionRepositoryActionsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php',
'DiffusionRepositoryAutomationManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php',
'DiffusionRepositoryBasicsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php',
'DiffusionRepositoryBranchesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php',
'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
'DiffusionRepositoryClusterEngine' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php',
'DiffusionRepositoryClusterEngineLogInterface' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php',
'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php',
'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
'DiffusionRepositoryEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositoryEditConduitAPIMethod.php',
'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php',
'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php',
'DiffusionRepositoryEditEngine' => 'applications/diffusion/editor/DiffusionRepositoryEditEngine.php',
'DiffusionRepositoryEditEnormousController' => 'applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php',
'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php',
'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php',
'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php',
+ 'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php',
+ 'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php',
'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php',
'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php',
'DiffusionRepositoryManagePanelsController' => 'applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php',
'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php',
'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php',
'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php',
'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php',
'DiffusionRepositoryStagingManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php',
'DiffusionRepositoryStorageManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php',
'DiffusionRepositorySubversionManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php',
'DiffusionRepositorySymbolsManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php',
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
'DiffusionRepositoryURICredentialController' => 'applications/diffusion/controller/DiffusionRepositoryURICredentialController.php',
'DiffusionRepositoryURIDisableController' => 'applications/diffusion/controller/DiffusionRepositoryURIDisableController.php',
'DiffusionRepositoryURIEditController' => 'applications/diffusion/controller/DiffusionRepositoryURIEditController.php',
'DiffusionRepositoryURIViewController' => 'applications/diffusion/controller/DiffusionRepositoryURIViewController.php',
'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php',
'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsSearchEngineAttachment.php',
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php',
'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php',
'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php',
'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php',
'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php',
'DiffusionSvnBlameQuery' => 'applications/diffusion/query/blame/DiffusionSvnBlameQuery.php',
'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php',
'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php',
'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php',
'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php',
'DiffusionSymbolDatasource' => 'applications/diffusion/typeahead/DiffusionSymbolDatasource.php',
'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php',
'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
'DiffusionTagTableView' => 'applications/diffusion/view/DiffusionTagTableView.php',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php',
'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php',
'DiffusionURIEditEngine' => 'applications/diffusion/editor/DiffusionURIEditEngine.php',
'DiffusionURIEditor' => 'applications/diffusion/editor/DiffusionURIEditor.php',
'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php',
'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php',
'DiffusionView' => 'applications/diffusion/view/DiffusionView.php',
'DivinerArticleAtomizer' => 'applications/diviner/atomizer/DivinerArticleAtomizer.php',
'DivinerAtom' => 'applications/diviner/atom/DivinerAtom.php',
'DivinerAtomCache' => 'applications/diviner/cache/DivinerAtomCache.php',
'DivinerAtomController' => 'applications/diviner/controller/DivinerAtomController.php',
'DivinerAtomListController' => 'applications/diviner/controller/DivinerAtomListController.php',
'DivinerAtomPHIDType' => 'applications/diviner/phid/DivinerAtomPHIDType.php',
'DivinerAtomQuery' => 'applications/diviner/query/DivinerAtomQuery.php',
'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
'DivinerAtomSearchEngine' => 'applications/diviner/query/DivinerAtomSearchEngine.php',
'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php',
'DivinerBookDatasource' => 'applications/diviner/typeahead/DivinerBookDatasource.php',
'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php',
'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php',
'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php',
'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php',
'DivinerController' => 'applications/diviner/controller/DivinerController.php',
'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php',
'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php',
'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php',
'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php',
'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php',
'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php',
'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php',
'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php',
'DivinerLiveBookFulltextEngine' => 'applications/diviner/search/DivinerLiveBookFulltextEngine.php',
'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php',
'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php',
'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php',
'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php',
'DivinerLiveSymbolFulltextEngine' => 'applications/diviner/search/DivinerLiveSymbolFulltextEngine.php',
'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php',
'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php',
'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php',
'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php',
'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php',
'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php',
'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php',
'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php',
'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php',
'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php',
'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php',
'DoorkeeperBridgeGitHubUser' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubUser.php',
'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php',
'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php',
'DoorkeeperBridgedObjectCurtainExtension' => 'applications/doorkeeper/engineextension/DoorkeeperBridgedObjectCurtainExtension.php',
'DoorkeeperBridgedObjectInterface' => 'applications/doorkeeper/bridge/DoorkeeperBridgedObjectInterface.php',
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
'DoorkeeperExternalObjectPHIDType' => 'applications/doorkeeper/phid/DoorkeeperExternalObjectPHIDType.php',
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php',
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php',
'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php',
'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php',
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php',
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php',
'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php',
'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php',
'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php',
'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php',
'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php',
'DrydockAuthorizationSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php',
'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php',
'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php',
'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php',
'DrydockBlueprintDatasource' => 'applications/drydock/typeahead/DrydockBlueprintDatasource.php',
'DrydockBlueprintDisableController' => 'applications/drydock/controller/DrydockBlueprintDisableController.php',
'DrydockBlueprintDisableTransaction' => 'applications/drydock/xaction/DrydockBlueprintDisableTransaction.php',
'DrydockBlueprintEditConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintEditConduitAPIMethod.php',
'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php',
'DrydockBlueprintEditEngine' => 'applications/drydock/editor/DrydockBlueprintEditEngine.php',
'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php',
'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php',
'DrydockBlueprintImplementationTestCase' => 'applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php',
'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php',
'DrydockBlueprintNameNgrams' => 'applications/drydock/storage/DrydockBlueprintNameNgrams.php',
'DrydockBlueprintNameTransaction' => 'applications/drydock/xaction/DrydockBlueprintNameTransaction.php',
'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php',
'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php',
'DrydockBlueprintSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php',
'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php',
'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php',
'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
'DrydockBlueprintTransactionType' => 'applications/drydock/xaction/DrydockBlueprintTransactionType.php',
'DrydockBlueprintTypeTransaction' => 'applications/drydock/xaction/DrydockBlueprintTypeTransaction.php',
'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php',
'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php',
'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php',
'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
'DrydockController' => 'applications/drydock/controller/DrydockController.php',
'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php',
'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php',
'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php',
'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php',
'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php',
'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php',
'DrydockLeaseActivationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php',
'DrydockLeaseActivationYieldLogType' => 'applications/drydock/logtype/DrydockLeaseActivationYieldLogType.php',
'DrydockLeaseAllocationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php',
'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php',
'DrydockLeaseDatasource' => 'applications/drydock/typeahead/DrydockLeaseDatasource.php',
'DrydockLeaseDestroyedLogType' => 'applications/drydock/logtype/DrydockLeaseDestroyedLogType.php',
'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php',
'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php',
'DrydockLeaseNoAuthorizationsLogType' => 'applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php',
'DrydockLeaseNoBlueprintsLogType' => 'applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php',
'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php',
'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php',
'DrydockLeaseQueuedLogType' => 'applications/drydock/logtype/DrydockLeaseQueuedLogType.php',
'DrydockLeaseReacquireLogType' => 'applications/drydock/logtype/DrydockLeaseReacquireLogType.php',
'DrydockLeaseReclaimLogType' => 'applications/drydock/logtype/DrydockLeaseReclaimLogType.php',
'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php',
'DrydockLeaseReleasedLogType' => 'applications/drydock/logtype/DrydockLeaseReleasedLogType.php',
'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
'DrydockLeaseUpdateWorker' => 'applications/drydock/worker/DrydockLeaseUpdateWorker.php',
'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
'DrydockLeaseWaitingForResourcesLogType' => 'applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php',
'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
'DrydockLogGarbageCollector' => 'applications/drydock/garbagecollector/DrydockLogGarbageCollector.php',
'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php',
'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
'DrydockLogType' => 'applications/drydock/logtype/DrydockLogType.php',
'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php',
'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
'DrydockManagementReclaimWorkflow' => 'applications/drydock/management/DrydockManagementReclaimWorkflow.php',
'DrydockManagementReleaseLeaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php',
'DrydockManagementReleaseResourceWorkflow' => 'applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php',
'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php',
'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php',
'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php',
'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php',
'DrydockRepositoryOperationController' => 'applications/drydock/controller/DrydockRepositoryOperationController.php',
'DrydockRepositoryOperationDismissController' => 'applications/drydock/controller/DrydockRepositoryOperationDismissController.php',
'DrydockRepositoryOperationListController' => 'applications/drydock/controller/DrydockRepositoryOperationListController.php',
'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php',
'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php',
'DrydockRepositoryOperationSearchEngine' => 'applications/drydock/query/DrydockRepositoryOperationSearchEngine.php',
'DrydockRepositoryOperationStatusController' => 'applications/drydock/controller/DrydockRepositoryOperationStatusController.php',
'DrydockRepositoryOperationStatusView' => 'applications/drydock/view/DrydockRepositoryOperationStatusView.php',
'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php',
'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php',
'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php',
'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php',
'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php',
'DrydockResourceAllocationFailureLogType' => 'applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php',
'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php',
'DrydockResourceDatasource' => 'applications/drydock/typeahead/DrydockResourceDatasource.php',
'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php',
'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php',
'DrydockResourceLockException' => 'applications/drydock/exception/DrydockResourceLockException.php',
'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php',
'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php',
'DrydockResourceReclaimLogType' => 'applications/drydock/logtype/DrydockResourceReclaimLogType.php',
'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php',
'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php',
'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php',
'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php',
'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php',
'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
'DrydockSchemaSpec' => 'applications/drydock/storage/DrydockSchemaSpec.php',
'DrydockSlotLock' => 'applications/drydock/storage/DrydockSlotLock.php',
'DrydockSlotLockException' => 'applications/drydock/exception/DrydockSlotLockException.php',
'DrydockSlotLockFailureLogType' => 'applications/drydock/logtype/DrydockSlotLockFailureLogType.php',
'DrydockTestRepositoryOperation' => 'applications/drydock/operation/DrydockTestRepositoryOperation.php',
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php',
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php',
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php',
'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php',
'FeedQueryConduitAPIMethod' => 'applications/feed/conduit/FeedQueryConduitAPIMethod.php',
'FeedStoryNotificationGarbageCollector' => 'applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php',
'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php',
'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php',
'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php',
'FileDeletionWorker' => 'applications/files/worker/FileDeletionWorker.php',
'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php',
'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php',
'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php',
'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php',
'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
'FileTypeIcon' => 'applications/files/constants/FileTypeIcon.php',
'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php',
'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php',
'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php',
'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php',
'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php',
'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php',
'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php',
'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php',
'FundBacker' => 'applications/fund/storage/FundBacker.php',
'FundBackerCart' => 'applications/fund/phortune/FundBackerCart.php',
'FundBackerEditor' => 'applications/fund/editor/FundBackerEditor.php',
'FundBackerListController' => 'applications/fund/controller/FundBackerListController.php',
'FundBackerPHIDType' => 'applications/fund/phid/FundBackerPHIDType.php',
'FundBackerProduct' => 'applications/fund/phortune/FundBackerProduct.php',
'FundBackerQuery' => 'applications/fund/query/FundBackerQuery.php',
'FundBackerRefundTransaction' => 'applications/fund/xaction/FundBackerRefundTransaction.php',
'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php',
'FundBackerStatusTransaction' => 'applications/fund/xaction/FundBackerStatusTransaction.php',
'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php',
'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.php',
'FundBackerTransactionType' => 'applications/fund/xaction/FundBackerTransactionType.php',
'FundController' => 'applications/fund/controller/FundController.php',
'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php',
'FundDAO' => 'applications/fund/storage/FundDAO.php',
'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php',
'FundInitiative' => 'applications/fund/storage/FundInitiative.php',
'FundInitiativeBackController' => 'applications/fund/controller/FundInitiativeBackController.php',
'FundInitiativeBackerTransaction' => 'applications/fund/xaction/FundInitiativeBackerTransaction.php',
'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php',
'FundInitiativeDescriptionTransaction' => 'applications/fund/xaction/FundInitiativeDescriptionTransaction.php',
'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php',
'FundInitiativeEditEngine' => 'applications/fund/editor/FundInitiativeEditEngine.php',
'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php',
'FundInitiativeFerretEngine' => 'applications/fund/search/FundInitiativeFerretEngine.php',
'FundInitiativeFulltextEngine' => 'applications/fund/search/FundInitiativeFulltextEngine.php',
'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php',
'FundInitiativeMerchantTransaction' => 'applications/fund/xaction/FundInitiativeMerchantTransaction.php',
'FundInitiativeNameTransaction' => 'applications/fund/xaction/FundInitiativeNameTransaction.php',
'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php',
'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php',
'FundInitiativeRefundTransaction' => 'applications/fund/xaction/FundInitiativeRefundTransaction.php',
'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php',
'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php',
'FundInitiativeRisksTransaction' => 'applications/fund/xaction/FundInitiativeRisksTransaction.php',
'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php',
'FundInitiativeStatusTransaction' => 'applications/fund/xaction/FundInitiativeStatusTransaction.php',
'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php',
'FundInitiativeTransactionComment' => 'applications/fund/storage/FundInitiativeTransactionComment.php',
'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php',
'FundInitiativeTransactionType' => 'applications/fund/xaction/FundInitiativeTransactionType.php',
'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php',
'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
+ 'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php',
'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php',
'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php',
'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php',
'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php',
'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
'HarbormasterBuildArcanistAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildArcanistAutoplan.php',
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php',
'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php',
'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php',
'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
'HarbormasterBuildInitiatorDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildInitiatorDatasource.php',
'HarbormasterBuildLintMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php',
'HarbormasterBuildListController' => 'applications/harbormaster/controller/HarbormasterBuildListController.php',
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php',
'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php',
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php',
'HarbormasterBuildLogSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php',
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php',
'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php',
'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php',
'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php',
'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php',
'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php',
'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php',
'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php',
'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php',
'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php',
'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php',
'HarbormasterBuildRequest' => 'applications/harbormaster/engine/HarbormasterBuildRequest.php',
'HarbormasterBuildSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildSearchConduitAPIMethod.php',
'HarbormasterBuildSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildSearchEngine.php',
'HarbormasterBuildStatus' => 'applications/harbormaster/constants/HarbormasterBuildStatus.php',
'HarbormasterBuildStatusDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildStatusDatasource.php',
'HarbormasterBuildStep' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStep.php',
'HarbormasterBuildStepCoreCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php',
'HarbormasterBuildStepCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php',
'HarbormasterBuildStepEditor' => 'applications/harbormaster/editor/HarbormasterBuildStepEditor.php',
'HarbormasterBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php',
'HarbormasterBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildStepImplementation.php',
'HarbormasterBuildStepImplementationTestCase' => 'applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php',
'HarbormasterBuildStepPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php',
'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php',
'HarbormasterBuildStepTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php',
'HarbormasterBuildStepTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildStepTransactionQuery.php',
'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php',
'HarbormasterBuildTargetPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildTargetPHIDType.php',
'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php',
'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php',
'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php',
'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php',
'HarbormasterBuildableAdapterInterface' => 'applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php',
'HarbormasterBuildableEngine' => 'applications/harbormaster/engine/HarbormasterBuildableEngine.php',
'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php',
'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php',
'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php',
'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php',
'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php',
'HarbormasterBuildableStatus' => 'applications/harbormaster/constants/HarbormasterBuildableStatus.php',
'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php',
'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php',
'HarbormasterBuildkiteBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildkiteBuildableInterface.php',
'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php',
'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php',
'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php',
'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php',
'HarbormasterCircleCIHookController' => 'applications/harbormaster/controller/HarbormasterCircleCIHookController.php',
'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
+ 'HarbormasterControlBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php',
'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php',
'HarbormasterCreatePlansCapability' => 'applications/harbormaster/capability/HarbormasterCreatePlansCapability.php',
'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
'HarbormasterDrydockBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterDrydockBuildStepGroup.php',
'HarbormasterDrydockCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php',
'HarbormasterDrydockLeaseArtifact' => 'applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php',
'HarbormasterExecFuture' => 'applications/harbormaster/future/HarbormasterExecFuture.php',
'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php',
'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php',
'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php',
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
'HarbormasterLogWorker' => 'applications/harbormaster/worker/HarbormasterLogWorker.php',
'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php',
'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
'HarbormasterManagementPublishWorkflow' => 'applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php',
'HarbormasterManagementRebuildLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php',
'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php',
'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php',
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php',
'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php',
'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php',
'HarbormasterPrototypeBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterPrototypeBuildStepGroup.php',
'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php',
'HarbormasterQueryAutotargetsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryAutotargetsConduitAPIMethod.php',
'HarbormasterQueryBuildablesConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php',
'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php',
'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php',
'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php',
'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php',
'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php',
'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php',
'HarbormasterSendMessageConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php',
'HarbormasterSleepBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php',
'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php',
'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
'HarbormasterStepViewController' => 'applications/harbormaster/controller/HarbormasterStepViewController.php',
'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.php',
'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
'HarbormasterTestBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterTestBuildStepGroup.php',
'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
'HarbormasterURIArtifact' => 'applications/harbormaster/artifact/HarbormasterURIArtifact.php',
'HarbormasterUnitMessageListController' => 'applications/harbormaster/controller/HarbormasterUnitMessageListController.php',
'HarbormasterUnitMessageViewController' => 'applications/harbormaster/controller/HarbormasterUnitMessageViewController.php',
'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php',
'HarbormasterUnitStatus' => 'applications/harbormaster/constants/HarbormasterUnitStatus.php',
'HarbormasterUnitSummaryView' => 'applications/harbormaster/view/HarbormasterUnitSummaryView.php',
'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php',
'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php',
'HarbormasterWorkingCopyArtifact' => 'applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php',
'HeraldActingUserField' => 'applications/herald/field/HeraldActingUserField.php',
'HeraldAction' => 'applications/herald/action/HeraldAction.php',
'HeraldActionGroup' => 'applications/herald/action/HeraldActionGroup.php',
'HeraldActionRecord' => 'applications/herald/storage/HeraldActionRecord.php',
'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php',
+ 'HeraldAdapterDatasource' => 'applications/herald/typeahead/HeraldAdapterDatasource.php',
'HeraldAlwaysField' => 'applications/herald/field/HeraldAlwaysField.php',
'HeraldAnotherRuleField' => 'applications/herald/field/HeraldAnotherRuleField.php',
'HeraldApplicationActionGroup' => 'applications/herald/action/HeraldApplicationActionGroup.php',
'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php',
'HeraldBasicFieldGroup' => 'applications/herald/field/HeraldBasicFieldGroup.php',
'HeraldBuildableState' => 'applications/herald/state/HeraldBuildableState.php',
'HeraldCallWebhookAction' => 'applications/herald/action/HeraldCallWebhookAction.php',
'HeraldCommentAction' => 'applications/herald/action/HeraldCommentAction.php',
'HeraldCommitAdapter' => 'applications/diffusion/herald/HeraldCommitAdapter.php',
'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php',
'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php',
'HeraldController' => 'applications/herald/controller/HeraldController.php',
'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php',
'HeraldCreateWebhooksCapability' => 'applications/herald/capability/HeraldCreateWebhooksCapability.php',
'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php',
'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php',
'HeraldDifferentialDiffAdapter' => 'applications/differential/herald/HeraldDifferentialDiffAdapter.php',
'HeraldDifferentialRevisionAdapter' => 'applications/differential/herald/HeraldDifferentialRevisionAdapter.php',
'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
'HeraldDoNothingAction' => 'applications/herald/action/HeraldDoNothingAction.php',
'HeraldEditFieldGroup' => 'applications/herald/field/HeraldEditFieldGroup.php',
'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php',
'HeraldEmptyFieldValue' => 'applications/herald/value/HeraldEmptyFieldValue.php',
'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php',
'HeraldExactProjectsField' => 'applications/project/herald/HeraldExactProjectsField.php',
'HeraldField' => 'applications/herald/field/HeraldField.php',
'HeraldFieldGroup' => 'applications/herald/field/HeraldFieldGroup.php',
'HeraldFieldTestCase' => 'applications/herald/field/__tests__/HeraldFieldTestCase.php',
'HeraldFieldValue' => 'applications/herald/value/HeraldFieldValue.php',
'HeraldGroup' => 'applications/herald/group/HeraldGroup.php',
'HeraldInvalidActionException' => 'applications/herald/engine/exception/HeraldInvalidActionException.php',
'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php',
'HeraldMailableState' => 'applications/herald/state/HeraldMailableState.php',
'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php',
'HeraldManiphestTaskAdapter' => 'applications/maniphest/herald/HeraldManiphestTaskAdapter.php',
'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php',
'HeraldNewObjectField' => 'applications/herald/field/HeraldNewObjectField.php',
'HeraldNotifyActionGroup' => 'applications/herald/action/HeraldNotifyActionGroup.php',
'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php',
'HeraldPhameBlogAdapter' => 'applications/phame/herald/HeraldPhameBlogAdapter.php',
'HeraldPhamePostAdapter' => 'applications/phame/herald/HeraldPhamePostAdapter.php',
'HeraldPholioMockAdapter' => 'applications/pholio/herald/HeraldPholioMockAdapter.php',
'HeraldPonderQuestionAdapter' => 'applications/ponder/herald/HeraldPonderQuestionAdapter.php',
'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php',
'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php',
'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php',
'HeraldPreventActionGroup' => 'applications/herald/action/HeraldPreventActionGroup.php',
'HeraldProjectsField' => 'applications/project/herald/HeraldProjectsField.php',
'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php',
'HeraldRelatedFieldGroup' => 'applications/herald/field/HeraldRelatedFieldGroup.php',
'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php',
'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php',
'HeraldRule' => 'applications/herald/storage/HeraldRule.php',
+ 'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php',
+ 'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php',
'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php',
'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
+ 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php',
+ 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php',
'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
+ 'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php',
'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php',
'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php',
'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php',
'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php',
'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php',
'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php',
+ 'HeraldRuleTypeDatasource' => 'applications/herald/typeahead/HeraldRuleTypeDatasource.php',
+ 'HeraldRuleTypeField' => 'applications/herald/field/rule/HeraldRuleTypeField.php',
'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php',
'HeraldSchemaSpec' => 'applications/herald/storage/HeraldSchemaSpec.php',
'HeraldSelectFieldValue' => 'applications/herald/value/HeraldSelectFieldValue.php',
'HeraldSpaceField' => 'applications/spaces/herald/HeraldSpaceField.php',
'HeraldState' => 'applications/herald/state/HeraldState.php',
'HeraldStateReasons' => 'applications/herald/state/HeraldStateReasons.php',
'HeraldSubscribersField' => 'applications/subscriptions/herald/HeraldSubscribersField.php',
'HeraldSupportActionGroup' => 'applications/herald/action/HeraldSupportActionGroup.php',
'HeraldSupportFieldGroup' => 'applications/herald/field/HeraldSupportFieldGroup.php',
'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php',
'HeraldTextFieldValue' => 'applications/herald/value/HeraldTextFieldValue.php',
'HeraldTokenizerFieldValue' => 'applications/herald/value/HeraldTokenizerFieldValue.php',
'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php',
'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php',
'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php',
'HeraldTranscriptDestructionEngineExtension' => 'applications/herald/engineextension/HeraldTranscriptDestructionEngineExtension.php',
'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php',
'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php',
'HeraldTranscriptPHIDType' => 'applications/herald/phid/HeraldTranscriptPHIDType.php',
'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php',
'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
'HeraldUtilityActionGroup' => 'applications/herald/action/HeraldUtilityActionGroup.php',
'HeraldWebhook' => 'applications/herald/storage/HeraldWebhook.php',
'HeraldWebhookCallManagementWorkflow' => 'applications/herald/management/HeraldWebhookCallManagementWorkflow.php',
'HeraldWebhookController' => 'applications/herald/controller/HeraldWebhookController.php',
'HeraldWebhookDatasource' => 'applications/herald/typeahead/HeraldWebhookDatasource.php',
'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php',
'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php',
'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php',
'HeraldWebhookKeyController' => 'applications/herald/controller/HeraldWebhookKeyController.php',
'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php',
'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php',
'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php',
'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php',
'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php',
'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php',
'HeraldWebhookRequestGarbageCollector' => 'applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php',
'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php',
'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php',
'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php',
'HeraldWebhookSearchEngine' => 'applications/herald/query/HeraldWebhookSearchEngine.php',
'HeraldWebhookStatusTransaction' => 'applications/herald/xaction/HeraldWebhookStatusTransaction.php',
'HeraldWebhookTestController' => 'applications/herald/controller/HeraldWebhookTestController.php',
'HeraldWebhookTransaction' => 'applications/herald/storage/HeraldWebhookTransaction.php',
'HeraldWebhookTransactionQuery' => 'applications/herald/query/HeraldWebhookTransactionQuery.php',
'HeraldWebhookTransactionType' => 'applications/herald/xaction/HeraldWebhookTransactionType.php',
'HeraldWebhookURITransaction' => 'applications/herald/xaction/HeraldWebhookURITransaction.php',
'HeraldWebhookViewController' => 'applications/herald/controller/HeraldWebhookViewController.php',
'HeraldWebhookWorker' => 'applications/herald/worker/HeraldWebhookWorker.php',
'Javelin' => 'infrastructure/javelin/Javelin.php',
'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php',
'LegalpadDefaultEditCapability' => 'applications/legalpad/capability/LegalpadDefaultEditCapability.php',
'LegalpadDefaultViewCapability' => 'applications/legalpad/capability/LegalpadDefaultViewCapability.php',
'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php',
'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php',
'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php',
'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php',
'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php',
'LegalpadDocumentEditEngine' => 'applications/legalpad/editor/LegalpadDocumentEditEngine.php',
'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php',
'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php',
'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php',
'LegalpadDocumentPreambleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentPreambleTransaction.php',
'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php',
'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.php',
'LegalpadDocumentRequireSignatureTransaction' => 'applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php',
'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php',
'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php',
'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php',
'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php',
'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php',
'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php',
'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php',
'LegalpadDocumentSignatureTypeTransaction' => 'applications/legalpad/xaction/LegalpadDocumentSignatureTypeTransaction.php',
'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php',
'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php',
'LegalpadDocumentTextTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTextTransaction.php',
'LegalpadDocumentTitleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTitleTransaction.php',
'LegalpadDocumentTransactionType' => 'applications/legalpad/xaction/LegalpadDocumentTransactionType.php',
'LegalpadMailReceiver' => 'applications/legalpad/mail/LegalpadMailReceiver.php',
'LegalpadObjectNeedsSignatureEdgeType' => 'applications/legalpad/edge/LegalpadObjectNeedsSignatureEdgeType.php',
'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php',
'LegalpadRequireSignatureHeraldAction' => 'applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php',
'LegalpadSchemaSpec' => 'applications/legalpad/storage/LegalpadSchemaSpec.php',
'LegalpadSignatureNeededByObjectEdgeType' => 'applications/legalpad/edge/LegalpadSignatureNeededByObjectEdgeType.php',
'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php',
'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php',
'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php',
'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php',
'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php',
'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php',
'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php',
'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php',
'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php',
'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php',
'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php',
'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php',
'MacroConduitAPIMethod' => 'applications/macro/conduit/MacroConduitAPIMethod.php',
'MacroCreateMemeConduitAPIMethod' => 'applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php',
'MacroEditConduitAPIMethod' => 'applications/macro/conduit/MacroEditConduitAPIMethod.php',
'MacroEmojiExample' => 'applications/uiexample/examples/MacroEmojiExample.php',
'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php',
'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php',
'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php',
'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php',
'ManiphestCreateMailReceiver' => 'applications/maniphest/mail/ManiphestCreateMailReceiver.php',
'ManiphestCreateTaskConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php',
'ManiphestCustomField' => 'applications/maniphest/field/ManiphestCustomField.php',
'ManiphestCustomFieldNumericIndex' => 'applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php',
'ManiphestCustomFieldStatusParser' => 'applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php',
'ManiphestCustomFieldStatusParserTestCase' => 'applications/maniphest/field/parser/__tests__/ManiphestCustomFieldStatusParserTestCase.php',
'ManiphestCustomFieldStorage' => 'applications/maniphest/storage/ManiphestCustomFieldStorage.php',
'ManiphestCustomFieldStringIndex' => 'applications/maniphest/storage/ManiphestCustomFieldStringIndex.php',
'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php',
'ManiphestDefaultEditCapability' => 'applications/maniphest/capability/ManiphestDefaultEditCapability.php',
'ManiphestDefaultViewCapability' => 'applications/maniphest/capability/ManiphestDefaultViewCapability.php',
- 'ManiphestEditAssignCapability' => 'applications/maniphest/capability/ManiphestEditAssignCapability.php',
'ManiphestEditConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestEditConduitAPIMethod.php',
'ManiphestEditEngine' => 'applications/maniphest/editor/ManiphestEditEngine.php',
- 'ManiphestEditPoliciesCapability' => 'applications/maniphest/capability/ManiphestEditPoliciesCapability.php',
- 'ManiphestEditPriorityCapability' => 'applications/maniphest/capability/ManiphestEditPriorityCapability.php',
- 'ManiphestEditProjectsCapability' => 'applications/maniphest/capability/ManiphestEditProjectsCapability.php',
- 'ManiphestEditStatusCapability' => 'applications/maniphest/capability/ManiphestEditStatusCapability.php',
'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php',
'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php',
'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php',
'ManiphestMailEngineExtension' => 'applications/maniphest/engineextension/ManiphestMailEngineExtension.php',
'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php',
'ManiphestPointsConfigType' => 'applications/maniphest/config/ManiphestPointsConfigType.php',
'ManiphestPrioritiesConfigType' => 'applications/maniphest/config/ManiphestPrioritiesConfigType.php',
'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php',
'ManiphestPrioritySearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestPrioritySearchConduitAPIMethod.php',
'ManiphestProjectNameFulltextEngineExtension' => 'applications/maniphest/engineextension/ManiphestProjectNameFulltextEngineExtension.php',
'ManiphestQueryConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php',
'ManiphestQueryStatusesConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php',
'ManiphestRemarkupRule' => 'applications/maniphest/remarkup/ManiphestRemarkupRule.php',
'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
'ManiphestSchemaSpec' => 'applications/maniphest/storage/ManiphestSchemaSpec.php',
'ManiphestSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestSearchConduitAPIMethod.php',
'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
'ManiphestStatusSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php',
'ManiphestStatusesConfigType' => 'applications/maniphest/config/ManiphestStatusesConfigType.php',
'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
'ManiphestSubtypesConfigType' => 'applications/maniphest/config/ManiphestSubtypesConfigType.php',
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
'ManiphestTaskAssignHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php',
'ManiphestTaskAssignOtherHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php',
'ManiphestTaskAssignSelfHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php',
'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php',
'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php',
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php',
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php',
'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
'ManiphestTaskDescriptionHeraldField' => 'applications/maniphest/herald/ManiphestTaskDescriptionHeraldField.php',
'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php',
'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php',
'ManiphestTaskEdgeTransaction' => 'applications/maniphest/xaction/ManiphestTaskEdgeTransaction.php',
'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php',
'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php',
'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php',
'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php',
'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php',
'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php',
'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php',
'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php',
'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php',
'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php',
'ManiphestTaskHasRevisionRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php',
'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php',
'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php',
'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php',
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php',
'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php',
'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php',
'ManiphestTaskMergedFromTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedFromTransaction.php',
'ManiphestTaskMergedIntoTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php',
'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php',
'ManiphestTaskOwnerTransaction' => 'applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php',
'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php',
'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php',
'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php',
'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php',
'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php',
'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php',
'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php',
'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php',
'ManiphestTaskPriorityHeraldField' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldField.php',
'ManiphestTaskPriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php',
'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php',
'ManiphestTaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskRelationship.php',
'ManiphestTaskRelationshipSource' => 'applications/search/relationship/ManiphestTaskRelationshipSource.php',
'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php',
'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php',
'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php',
'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php',
'ManiphestTaskStatusFunctionDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusFunctionDatasource.php',
'ManiphestTaskStatusHeraldAction' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php',
'ManiphestTaskStatusHeraldField' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldField.php',
'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php',
'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php',
'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php',
'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php',
'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php',
'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php',
'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php',
'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php',
'ManiphestTaskUnblockTransaction' => 'applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php',
'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php',
'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php',
'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php',
'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php',
'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php',
'ManiphestView' => 'applications/maniphest/view/ManiphestView.php',
'MetaMTAEmailTransactionCommand' => 'applications/metamta/command/MetaMTAEmailTransactionCommand.php',
'MetaMTAEmailTransactionCommandTestCase' => 'applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php',
'MetaMTAMailReceivedGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailReceivedGarbageCollector.php',
'MetaMTAMailSentGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php',
'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php',
'MultimeterContext' => 'applications/multimeter/storage/MultimeterContext.php',
'MultimeterControl' => 'applications/multimeter/data/MultimeterControl.php',
'MultimeterController' => 'applications/multimeter/controller/MultimeterController.php',
'MultimeterDAO' => 'applications/multimeter/storage/MultimeterDAO.php',
'MultimeterDimension' => 'applications/multimeter/storage/MultimeterDimension.php',
'MultimeterEvent' => 'applications/multimeter/storage/MultimeterEvent.php',
'MultimeterEventGarbageCollector' => 'applications/multimeter/garbagecollector/MultimeterEventGarbageCollector.php',
'MultimeterHost' => 'applications/multimeter/storage/MultimeterHost.php',
'MultimeterLabel' => 'applications/multimeter/storage/MultimeterLabel.php',
'MultimeterSampleController' => 'applications/multimeter/controller/MultimeterSampleController.php',
'MultimeterViewer' => 'applications/multimeter/storage/MultimeterViewer.php',
'NuanceCommandImplementation' => 'applications/nuance/command/NuanceCommandImplementation.php',
'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php',
'NuanceConsoleController' => 'applications/nuance/controller/NuanceConsoleController.php',
'NuanceContentSource' => 'applications/nuance/contentsource/NuanceContentSource.php',
'NuanceController' => 'applications/nuance/controller/NuanceController.php',
'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php',
'NuanceFormItemType' => 'applications/nuance/item/NuanceFormItemType.php',
'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php',
'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php',
'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php',
'NuanceGitHubRawEvent' => 'applications/nuance/github/NuanceGitHubRawEvent.php',
'NuanceGitHubRawEventTestCase' => 'applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php',
'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php',
'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php',
'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php',
'NuanceImportCursorData' => 'applications/nuance/storage/NuanceImportCursorData.php',
'NuanceImportCursorDataQuery' => 'applications/nuance/query/NuanceImportCursorDataQuery.php',
'NuanceImportCursorPHIDType' => 'applications/nuance/phid/NuanceImportCursorPHIDType.php',
'NuanceItem' => 'applications/nuance/storage/NuanceItem.php',
'NuanceItemActionController' => 'applications/nuance/controller/NuanceItemActionController.php',
'NuanceItemCommand' => 'applications/nuance/storage/NuanceItemCommand.php',
'NuanceItemCommandQuery' => 'applications/nuance/query/NuanceItemCommandQuery.php',
'NuanceItemCommandSpec' => 'applications/nuance/command/NuanceItemCommandSpec.php',
'NuanceItemCommandTransaction' => 'applications/nuance/xaction/NuanceItemCommandTransaction.php',
'NuanceItemController' => 'applications/nuance/controller/NuanceItemController.php',
'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php',
'NuanceItemListController' => 'applications/nuance/controller/NuanceItemListController.php',
'NuanceItemManageController' => 'applications/nuance/controller/NuanceItemManageController.php',
'NuanceItemOwnerTransaction' => 'applications/nuance/xaction/NuanceItemOwnerTransaction.php',
'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php',
'NuanceItemPropertyTransaction' => 'applications/nuance/xaction/NuanceItemPropertyTransaction.php',
'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php',
'NuanceItemQueueTransaction' => 'applications/nuance/xaction/NuanceItemQueueTransaction.php',
'NuanceItemRequestorTransaction' => 'applications/nuance/xaction/NuanceItemRequestorTransaction.php',
'NuanceItemSearchEngine' => 'applications/nuance/query/NuanceItemSearchEngine.php',
'NuanceItemSourceTransaction' => 'applications/nuance/xaction/NuanceItemSourceTransaction.php',
'NuanceItemStatusTransaction' => 'applications/nuance/xaction/NuanceItemStatusTransaction.php',
'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php',
'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php',
'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php',
'NuanceItemTransactionType' => 'applications/nuance/xaction/NuanceItemTransactionType.php',
'NuanceItemType' => 'applications/nuance/item/NuanceItemType.php',
'NuanceItemUpdateWorker' => 'applications/nuance/worker/NuanceItemUpdateWorker.php',
'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php',
'NuanceManagementImportWorkflow' => 'applications/nuance/management/NuanceManagementImportWorkflow.php',
'NuanceManagementUpdateWorkflow' => 'applications/nuance/management/NuanceManagementUpdateWorkflow.php',
'NuanceManagementWorkflow' => 'applications/nuance/management/NuanceManagementWorkflow.php',
'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php',
'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php',
'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php',
'NuanceQueueController' => 'applications/nuance/controller/NuanceQueueController.php',
'NuanceQueueDatasource' => 'applications/nuance/typeahead/NuanceQueueDatasource.php',
'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php',
'NuanceQueueEditEngine' => 'applications/nuance/editor/NuanceQueueEditEngine.php',
'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php',
'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.php',
'NuanceQueueNameTransaction' => 'applications/nuance/xaction/NuanceQueueNameTransaction.php',
'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php',
'NuanceQueueQuery' => 'applications/nuance/query/NuanceQueueQuery.php',
'NuanceQueueSearchEngine' => 'applications/nuance/query/NuanceQueueSearchEngine.php',
'NuanceQueueTransaction' => 'applications/nuance/storage/NuanceQueueTransaction.php',
'NuanceQueueTransactionComment' => 'applications/nuance/storage/NuanceQueueTransactionComment.php',
'NuanceQueueTransactionQuery' => 'applications/nuance/query/NuanceQueueTransactionQuery.php',
'NuanceQueueTransactionType' => 'applications/nuance/xaction/NuanceQueueTransactionType.php',
'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php',
'NuanceQueueWorkController' => 'applications/nuance/controller/NuanceQueueWorkController.php',
'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php',
'NuanceSource' => 'applications/nuance/storage/NuanceSource.php',
'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php',
'NuanceSourceController' => 'applications/nuance/controller/NuanceSourceController.php',
'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php',
'NuanceSourceDefaultQueueTransaction' => 'applications/nuance/xaction/NuanceSourceDefaultQueueTransaction.php',
'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php',
'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php',
'NuanceSourceDefinitionTestCase' => 'applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php',
'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php',
'NuanceSourceEditEngine' => 'applications/nuance/editor/NuanceSourceEditEngine.php',
'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php',
'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php',
'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php',
'NuanceSourceNameNgrams' => 'applications/nuance/storage/NuanceSourceNameNgrams.php',
'NuanceSourceNameTransaction' => 'applications/nuance/xaction/NuanceSourceNameTransaction.php',
'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php',
'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php',
'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php',
'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php',
'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php',
'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php',
'NuanceSourceTransactionType' => 'applications/nuance/xaction/NuanceSourceTransactionType.php',
'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php',
'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php',
'NuanceTrashCommand' => 'applications/nuance/command/NuanceTrashCommand.php',
'NuanceWorker' => 'applications/nuance/worker/NuanceWorker.php',
'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php',
'OwnersEditConduitAPIMethod' => 'applications/owners/conduit/OwnersEditConduitAPIMethod.php',
'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php',
'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.php',
'OwnersSearchConduitAPIMethod' => 'applications/owners/conduit/OwnersSearchConduitAPIMethod.php',
'PHIDConduitAPIMethod' => 'applications/phid/conduit/PHIDConduitAPIMethod.php',
'PHIDInfoConduitAPIMethod' => 'applications/phid/conduit/PHIDInfoConduitAPIMethod.php',
'PHIDLookupConduitAPIMethod' => 'applications/phid/conduit/PHIDLookupConduitAPIMethod.php',
'PHIDQueryConduitAPIMethod' => 'applications/phid/conduit/PHIDQueryConduitAPIMethod.php',
'PHUI' => 'view/phui/PHUI.php',
'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php',
'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php',
'PHUIApplicationMenuView' => 'view/layout/PHUIApplicationMenuView.php',
'PHUIBadgeBoxView' => 'view/phui/PHUIBadgeBoxView.php',
'PHUIBadgeExample' => 'applications/uiexample/examples/PHUIBadgeExample.php',
'PHUIBadgeMiniView' => 'view/phui/PHUIBadgeMiniView.php',
'PHUIBadgeView' => 'view/phui/PHUIBadgeView.php',
'PHUIBigInfoExample' => 'applications/uiexample/examples/PHUIBigInfoExample.php',
'PHUIBigInfoView' => 'view/phui/PHUIBigInfoView.php',
'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php',
'PHUIBoxView' => 'view/phui/PHUIBoxView.php',
'PHUIButtonBarExample' => 'applications/uiexample/examples/PHUIButtonBarExample.php',
'PHUIButtonBarView' => 'view/phui/PHUIButtonBarView.php',
'PHUIButtonExample' => 'applications/uiexample/examples/PHUIButtonExample.php',
'PHUIButtonView' => 'view/phui/PHUIButtonView.php',
'PHUICMSView' => 'view/phui/PHUICMSView.php',
'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php',
'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php',
'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php',
'PHUICalendarWeekView' => 'view/phui/calendar/PHUICalendarWeekView.php',
'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php',
'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php',
'PHUICrumbView' => 'view/phui/PHUICrumbView.php',
'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php',
'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php',
'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php',
'PHUICurtainView' => 'view/layout/PHUICurtainView.php',
'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php',
'PHUIDiffGraphViewTestCase' => 'infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php',
'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php',
'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php',
'PHUIDiffInlineCommentPreviewListView' => 'infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php',
'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php',
'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php',
'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php',
'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php',
'PHUIDiffInlineThreader' => 'infrastructure/diff/view/PHUIDiffInlineThreader.php',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php',
'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php',
'PHUIDiffTableOfContentsItemView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php',
'PHUIDiffTableOfContentsListView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsListView.php',
'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php',
'PHUIDocumentSummaryView' => 'view/phui/PHUIDocumentSummaryView.php',
- 'PHUIDocumentViewPro' => 'view/phui/PHUIDocumentViewPro.php',
+ 'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php',
'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php',
'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php',
'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php',
'PHUIFormFileControl' => 'view/form/control/PHUIFormFileControl.php',
'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php',
'PHUIFormIconSetControl' => 'view/form/control/PHUIFormIconSetControl.php',
'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php',
'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php',
'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php',
'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php',
'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php',
'PHUIHeadThingView' => 'view/phui/PHUIHeadThingView.php',
'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php',
'PHUIHomeView' => 'applications/home/view/PHUIHomeView.php',
'PHUIHovercardUIExample' => 'applications/uiexample/examples/PHUIHovercardUIExample.php',
'PHUIHovercardView' => 'view/phui/PHUIHovercardView.php',
'PHUIIconCircleView' => 'view/phui/PHUIIconCircleView.php',
'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php',
'PHUIIconView' => 'view/phui/PHUIIconView.php',
'PHUIImageMaskExample' => 'applications/uiexample/examples/PHUIImageMaskExample.php',
'PHUIImageMaskView' => 'view/phui/PHUIImageMaskView.php',
'PHUIInfoExample' => 'applications/uiexample/examples/PHUIInfoExample.php',
'PHUIInfoView' => 'view/phui/PHUIInfoView.php',
'PHUIInvisibleCharacterTestCase' => 'view/phui/__tests__/PHUIInvisibleCharacterTestCase.php',
'PHUIInvisibleCharacterView' => 'view/phui/PHUIInvisibleCharacterView.php',
'PHUILeftRightExample' => 'applications/uiexample/examples/PHUILeftRightExample.php',
'PHUILeftRightView' => 'view/phui/PHUILeftRightView.php',
'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php',
'PHUIListItemView' => 'view/phui/PHUIListItemView.php',
'PHUIListView' => 'view/phui/PHUIListView.php',
'PHUIListViewTestCase' => 'view/layout/__tests__/PHUIListViewTestCase.php',
'PHUIObjectBoxView' => 'view/phui/PHUIObjectBoxView.php',
'PHUIObjectItemListExample' => 'applications/uiexample/examples/PHUIObjectItemListExample.php',
'PHUIObjectItemListView' => 'view/phui/PHUIObjectItemListView.php',
'PHUIObjectItemView' => 'view/phui/PHUIObjectItemView.php',
'PHUIPagerView' => 'view/phui/PHUIPagerView.php',
'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php',
'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php',
'PHUIPolicySectionView' => 'applications/policy/view/PHUIPolicySectionView.php',
'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php',
'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php',
'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php',
'PHUIRemarkupImageView' => 'infrastructure/markup/view/PHUIRemarkupImageView.php',
'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php',
'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php',
'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php',
'PHUISegmentBarView' => 'view/phui/PHUISegmentBarView.php',
'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php',
'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php',
'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php',
'PHUITabGroupView' => 'view/phui/PHUITabGroupView.php',
'PHUITabView' => 'view/phui/PHUITabView.php',
'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php',
'PHUITagView' => 'view/phui/PHUITagView.php',
'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
'PHUITwoColumnView' => 'view/phui/PHUITwoColumnView.php',
'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
'PHUIUserAvailabilityView' => 'applications/calendar/view/PHUIUserAvailabilityView.php',
'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
'PHUIXComponentsExample' => 'applications/uiexample/examples/PHUIXComponentsExample.php',
'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php',
'PassphraseConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseConduitAPIMethod.php',
'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php',
'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php',
'PassphraseCredentialAuthorPolicyRule' => 'applications/passphrase/policyrule/PassphraseCredentialAuthorPolicyRule.php',
'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php',
'PassphraseCredentialConduitTransaction' => 'applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php',
'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php',
'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php',
'PassphraseCredentialDescriptionTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php',
'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php',
'PassphraseCredentialDestroyTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php',
'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php',
'PassphraseCredentialFerretEngine' => 'applications/passphrase/search/PassphraseCredentialFerretEngine.php',
'PassphraseCredentialFulltextEngine' => 'applications/passphrase/search/PassphraseCredentialFulltextEngine.php',
'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php',
'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php',
'PassphraseCredentialLockTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLockTransaction.php',
'PassphraseCredentialLookedAtTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php',
'PassphraseCredentialNameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialNameTransaction.php',
'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php',
'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php',
'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php',
'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php',
'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php',
'PassphraseCredentialSecretIDTransaction' => 'applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php',
'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php',
'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php',
'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php',
'PassphraseCredentialTransactionType' => 'applications/passphrase/xaction/PassphraseCredentialTransactionType.php',
'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php',
'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php',
'PassphraseCredentialUsernameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php',
'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
'PassphraseDefaultEditCapability' => 'applications/passphrase/capability/PassphraseDefaultEditCapability.php',
'PassphraseDefaultViewCapability' => 'applications/passphrase/capability/PassphraseDefaultViewCapability.php',
'PassphraseNoteCredentialType' => 'applications/passphrase/credentialtype/PassphraseNoteCredentialType.php',
'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php',
'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php',
'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php',
'PassphraseRemarkupRule' => 'applications/passphrase/remarkup/PassphraseRemarkupRule.php',
'PassphraseSSHGeneratedKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php',
'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php',
'PassphraseSSHPrivateKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php',
'PassphraseSSHPrivateKeyFileCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php',
'PassphraseSSHPrivateKeyTextCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php',
'PassphraseSchemaSpec' => 'applications/passphrase/storage/PassphraseSchemaSpec.php',
'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php',
'PassphraseTokenCredentialType' => 'applications/passphrase/credentialtype/PassphraseTokenCredentialType.php',
'PasteConduitAPIMethod' => 'applications/paste/conduit/PasteConduitAPIMethod.php',
'PasteCreateConduitAPIMethod' => 'applications/paste/conduit/PasteCreateConduitAPIMethod.php',
'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php',
'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php',
'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php',
'PasteEditConduitAPIMethod' => 'applications/paste/conduit/PasteEditConduitAPIMethod.php',
'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php',
'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php',
'PasteLanguageSelectDatasource' => 'applications/paste/typeahead/PasteLanguageSelectDatasource.php',
'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php',
'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php',
'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php',
'PasteSearchConduitAPIMethod' => 'applications/paste/conduit/PasteSearchConduitAPIMethod.php',
'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php',
'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php',
+ 'PeopleDisableUsersCapability' => 'applications/people/capability/PeopleDisableUsersCapability.php',
'PeopleHovercardEngineExtension' => 'applications/people/engineextension/PeopleHovercardEngineExtension.php',
'PeopleMainMenuBarExtension' => 'applications/people/engineextension/PeopleMainMenuBarExtension.php',
'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php',
'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php',
'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php',
'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php',
'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php',
'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php',
'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php',
'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php',
'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
'PhabricatorAdministratorsPolicyRule' => 'applications/people/policyrule/PhabricatorAdministratorsPolicyRule.php',
'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php',
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php',
'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php',
'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php',
'PhabricatorAphlictManagementStopWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php',
'PhabricatorAphlictManagementWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php',
'PhabricatorAphlictSetupCheck' => 'applications/notification/setup/PhabricatorAphlictSetupCheck.php',
'PhabricatorAphrontBarUIExample' => 'applications/uiexample/examples/PhabricatorAphrontBarUIExample.php',
'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php',
'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php',
'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php',
'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php',
'PhabricatorApplicationApplicationTransaction' => 'applications/meta/storage/PhabricatorApplicationApplicationTransaction.php',
'PhabricatorApplicationApplicationTransactionQuery' => 'applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php',
'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php',
'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php',
'PhabricatorApplicationConfigurationPanelTestCase' => 'applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php',
'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php',
'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php',
'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php',
'PhabricatorApplicationEditEngine' => 'applications/meta/editor/PhabricatorApplicationEditEngine.php',
'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php',
'PhabricatorApplicationEditor' => 'applications/meta/editor/PhabricatorApplicationEditor.php',
'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php',
'PhabricatorApplicationObjectMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php',
'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php',
'PhabricatorApplicationPolicyChangeTransaction' => 'applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php',
'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php',
'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php',
'PhabricatorApplicationSchemaSpec' => 'applications/meta/storage/PhabricatorApplicationSchemaSpec.php',
'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php',
'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php',
'PhabricatorApplicationSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php',
'PhabricatorApplicationSearchResultView' => 'applications/search/view/PhabricatorApplicationSearchResultView.php',
'PhabricatorApplicationTestCase' => 'applications/base/__tests__/PhabricatorApplicationTestCase.php',
'PhabricatorApplicationTransaction' => 'applications/transactions/storage/PhabricatorApplicationTransaction.php',
'PhabricatorApplicationTransactionComment' => 'applications/transactions/storage/PhabricatorApplicationTransactionComment.php',
'PhabricatorApplicationTransactionCommentEditController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php',
'PhabricatorApplicationTransactionCommentEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php',
'PhabricatorApplicationTransactionCommentHistoryController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentHistoryController.php',
'PhabricatorApplicationTransactionCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php',
'PhabricatorApplicationTransactionCommentQuoteController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentQuoteController.php',
'PhabricatorApplicationTransactionCommentRawController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRawController.php',
'PhabricatorApplicationTransactionCommentRemoveController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php',
'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php',
'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php',
'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php',
'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php',
'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php',
'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php',
'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php',
'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php',
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
'PhabricatorApplicationTransactionRemarkupPreviewController' => 'applications/transactions/controller/PhabricatorApplicationTransactionRemarkupPreviewController.php',
'PhabricatorApplicationTransactionReplyHandler' => 'applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php',
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php',
'PhabricatorApplicationTransactionStructureException' => 'applications/transactions/exception/PhabricatorApplicationTransactionStructureException.php',
'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php',
'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php',
'PhabricatorApplicationTransactionType' => 'applications/meta/xactions/PhabricatorApplicationTransactionType.php',
'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php',
'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php',
'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php',
'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php',
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
'PhabricatorApplicationTransactionWarningException' => 'applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php',
'PhabricatorApplicationTransactionWarningResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php',
'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php',
'PhabricatorApplicationUninstallTransaction' => 'applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php',
'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php',
'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php',
'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
'PhabricatorApplyEditField' => 'applications/transactions/editfield/PhabricatorApplyEditField.php',
'PhabricatorAsanaAuthProvider' => 'applications/auth/provider/PhabricatorAsanaAuthProvider.php',
'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php',
'PhabricatorAudioDocumentEngine' => 'applications/files/document/PhabricatorAudioDocumentEngine.php',
'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php',
'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php',
- 'PhabricatorAuditCommitStatusConstants' => 'applications/audit/constants/PhabricatorAuditCommitStatusConstants.php',
'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php',
'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php',
'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php',
'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php',
'PhabricatorAuditMailReceiver' => 'applications/audit/mail/PhabricatorAuditMailReceiver.php',
'PhabricatorAuditManagementDeleteWorkflow' => 'applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php',
'PhabricatorAuditManagementWorkflow' => 'applications/audit/management/PhabricatorAuditManagementWorkflow.php',
'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php',
'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php',
'PhabricatorAuditSynchronizeManagementWorkflow' => 'applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php',
'PhabricatorAuditTransaction' => 'applications/audit/storage/PhabricatorAuditTransaction.php',
'PhabricatorAuditTransactionComment' => 'applications/audit/storage/PhabricatorAuditTransactionComment.php',
'PhabricatorAuditTransactionQuery' => 'applications/audit/query/PhabricatorAuditTransactionQuery.php',
'PhabricatorAuditTransactionView' => 'applications/audit/view/PhabricatorAuditTransactionView.php',
'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'applications/audit/management/PhabricatorAuditUpdateOwnersManagementWorkflow.php',
'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php',
'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php',
'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php',
'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php',
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php',
'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php',
'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php',
'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php',
'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php',
'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php',
'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php',
'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php',
'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php',
'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php',
'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php',
'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php',
'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php',
'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php',
'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php',
'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php',
'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php',
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php',
'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php',
'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php',
'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php',
'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php',
'PhabricatorAuthManagementRevokeWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php',
'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php',
'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php',
'PhabricatorAuthManagementUnlimitWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUnlimitWorkflow.php',
'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php',
'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php',
'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php',
'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php',
'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php',
'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php',
'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php',
'PhabricatorAuthPasswordEngine' => 'applications/auth/engine/PhabricatorAuthPasswordEngine.php',
'PhabricatorAuthPasswordException' => 'applications/auth/password/PhabricatorAuthPasswordException.php',
'PhabricatorAuthPasswordHashInterface' => 'applications/auth/password/PhabricatorAuthPasswordHashInterface.php',
'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php',
'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php',
'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php',
'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php',
'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php',
'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php',
'PhabricatorAuthPasswordTransactionQuery' => 'applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php',
'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php',
'PhabricatorAuthPasswordUpgradeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordUpgradeTransaction.php',
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
'PhabricatorAuthProviderConfigEditor' => 'applications/auth/editor/PhabricatorAuthProviderConfigEditor.php',
'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php',
'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php',
'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php',
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
'PhabricatorAuthRevoker' => 'applications/auth/revoker/PhabricatorAuthRevoker.php',
'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
'PhabricatorAuthSSHKeyEditor' => 'applications/auth/editor/PhabricatorAuthSSHKeyEditor.php',
'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
'PhabricatorAuthSSHKeyListController' => 'applications/auth/controller/PhabricatorAuthSSHKeyListController.php',
'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php',
'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php',
'PhabricatorAuthSSHKeyRevokeController' => 'applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php',
'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php',
'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
'PhabricatorAuthSSHKeyTestCase' => 'applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php',
'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php',
'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php',
'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php',
'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php',
'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
'PhabricatorAuthSessionEngineExtension' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtension.php',
'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php',
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php',
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php',
'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php',
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php',
'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php',
'PhabricatorAuthTemporaryTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php',
'PhabricatorAuthTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenType.php',
'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php',
'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php',
'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php',
'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php',
'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php',
'PhabricatorBadgesArchiveController' => 'applications/badges/controller/PhabricatorBadgesArchiveController.php',
'PhabricatorBadgesAward' => 'applications/badges/storage/PhabricatorBadgesAward.php',
'PhabricatorBadgesAwardController' => 'applications/badges/controller/PhabricatorBadgesAwardController.php',
'PhabricatorBadgesAwardQuery' => 'applications/badges/query/PhabricatorBadgesAwardQuery.php',
'PhabricatorBadgesAwardTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesAwardTestDataGenerator.php',
'PhabricatorBadgesBadge' => 'applications/badges/storage/PhabricatorBadgesBadge.php',
'PhabricatorBadgesBadgeAwardTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeAwardTransaction.php',
'PhabricatorBadgesBadgeDescriptionTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeDescriptionTransaction.php',
'PhabricatorBadgesBadgeFlavorTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeFlavorTransaction.php',
'PhabricatorBadgesBadgeIconTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeIconTransaction.php',
'PhabricatorBadgesBadgeNameNgrams' => 'applications/badges/storage/PhabricatorBadgesBadgeNameNgrams.php',
'PhabricatorBadgesBadgeNameTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php',
'PhabricatorBadgesBadgeQualityTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeQualityTransaction.php',
'PhabricatorBadgesBadgeRevokeTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeRevokeTransaction.php',
'PhabricatorBadgesBadgeStatusTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeStatusTransaction.php',
'PhabricatorBadgesBadgeTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesBadgeTestDataGenerator.php',
'PhabricatorBadgesBadgeTransactionType' => 'applications/badges/xaction/PhabricatorBadgesBadgeTransactionType.php',
'PhabricatorBadgesCommentController' => 'applications/badges/controller/PhabricatorBadgesCommentController.php',
'PhabricatorBadgesController' => 'applications/badges/controller/PhabricatorBadgesController.php',
'PhabricatorBadgesCreateCapability' => 'applications/badges/capability/PhabricatorBadgesCreateCapability.php',
'PhabricatorBadgesDAO' => 'applications/badges/storage/PhabricatorBadgesDAO.php',
'PhabricatorBadgesDatasource' => 'applications/badges/typeahead/PhabricatorBadgesDatasource.php',
'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php',
'PhabricatorBadgesEditConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesEditConduitAPIMethod.php',
'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php',
'PhabricatorBadgesEditEngine' => 'applications/badges/editor/PhabricatorBadgesEditEngine.php',
'PhabricatorBadgesEditRecipientsController' => 'applications/badges/controller/PhabricatorBadgesEditRecipientsController.php',
'PhabricatorBadgesEditor' => 'applications/badges/editor/PhabricatorBadgesEditor.php',
'PhabricatorBadgesIconSet' => 'applications/badges/icon/PhabricatorBadgesIconSet.php',
'PhabricatorBadgesListController' => 'applications/badges/controller/PhabricatorBadgesListController.php',
'PhabricatorBadgesLootContextFreeGrammar' => 'applications/badges/lipsum/PhabricatorBadgesLootContextFreeGrammar.php',
'PhabricatorBadgesMailReceiver' => 'applications/badges/mail/PhabricatorBadgesMailReceiver.php',
'PhabricatorBadgesPHIDType' => 'applications/badges/phid/PhabricatorBadgesPHIDType.php',
'PhabricatorBadgesProfileController' => 'applications/badges/controller/PhabricatorBadgesProfileController.php',
'PhabricatorBadgesQuality' => 'applications/badges/constants/PhabricatorBadgesQuality.php',
'PhabricatorBadgesQuery' => 'applications/badges/query/PhabricatorBadgesQuery.php',
'PhabricatorBadgesRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRecipientsController.php',
'PhabricatorBadgesRecipientsListView' => 'applications/badges/view/PhabricatorBadgesRecipientsListView.php',
'PhabricatorBadgesRemoveRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php',
'PhabricatorBadgesReplyHandler' => 'applications/badges/mail/PhabricatorBadgesReplyHandler.php',
'PhabricatorBadgesSchemaSpec' => 'applications/badges/storage/PhabricatorBadgesSchemaSpec.php',
'PhabricatorBadgesSearchConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesSearchConduitAPIMethod.php',
'PhabricatorBadgesSearchEngine' => 'applications/badges/query/PhabricatorBadgesSearchEngine.php',
'PhabricatorBadgesTransaction' => 'applications/badges/storage/PhabricatorBadgesTransaction.php',
'PhabricatorBadgesTransactionComment' => 'applications/badges/storage/PhabricatorBadgesTransactionComment.php',
'PhabricatorBadgesTransactionQuery' => 'applications/badges/query/PhabricatorBadgesTransactionQuery.php',
'PhabricatorBadgesViewController' => 'applications/badges/controller/PhabricatorBadgesViewController.php',
'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php',
'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php',
'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php',
'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
'PhabricatorBoardColumnsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php',
'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php',
'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
'PhabricatorBoolConfigType' => 'applications/config/type/PhabricatorBoolConfigType.php',
'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php',
'PhabricatorBoolMailStamp' => 'applications/metamta/stamp/PhabricatorBoolMailStamp.php',
'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php',
'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php',
'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
'PhabricatorBulkManagementExportWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php',
'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php',
'PhabricatorCSVExportFormat' => 'infrastructure/export/format/PhabricatorCSVExportFormat.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php',
'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php',
'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php',
'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php',
'PhabricatorCachePurger' => 'applications/cache/purger/PhabricatorCachePurger.php',
'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php',
'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php',
'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php',
'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php',
'PhabricatorCachedClassMapQuery' => 'applications/cache/PhabricatorCachedClassMapQuery.php',
'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php',
'PhabricatorCachesTestCase' => 'applications/cache/__tests__/PhabricatorCachesTestCase.php',
'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php',
'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php',
'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php',
'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php',
'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php',
'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php',
'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php',
'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php',
'PhabricatorCalendarEventDeclineTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDeclineTransaction.php',
'PhabricatorCalendarEventDefaultEditCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultEditCapability.php',
'PhabricatorCalendarEventDefaultViewCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultViewCapability.php',
'PhabricatorCalendarEventDescriptionTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDescriptionTransaction.php',
'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php',
'PhabricatorCalendarEventEditConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventEditConduitAPIMethod.php',
'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php',
'PhabricatorCalendarEventEditEngine' => 'applications/calendar/editor/PhabricatorCalendarEventEditEngine.php',
'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php',
'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php',
'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php',
'PhabricatorCalendarEventFerretEngine' => 'applications/calendar/search/PhabricatorCalendarEventFerretEngine.php',
'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php',
'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php',
'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php',
'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php',
'PhabricatorCalendarEventHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldField.php',
'PhabricatorCalendarEventHeraldFieldGroup' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldFieldGroup.php',
'PhabricatorCalendarEventHostPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventHostPolicyRule.php',
'PhabricatorCalendarEventHostTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventHostTransaction.php',
'PhabricatorCalendarEventIconTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventIconTransaction.php',
'PhabricatorCalendarEventInviteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php',
'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php',
'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php',
'PhabricatorCalendarEventInviteesPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php',
'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php',
'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php',
'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php',
'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php',
'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php',
'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
'PhabricatorCalendarEventPolicyCodex' => 'applications/calendar/codex/PhabricatorCalendarEventPolicyCodex.php',
'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
'PhabricatorCalendarEventRecurringTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventRecurringTransaction.php',
'PhabricatorCalendarEventReplyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventReplyTransaction.php',
'PhabricatorCalendarEventSearchConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventSearchConduitAPIMethod.php',
'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php',
'PhabricatorCalendarEventStartDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php',
'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php',
'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php',
'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php',
'PhabricatorCalendarEventTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarEventTransactionType.php',
'PhabricatorCalendarEventUntilDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php',
'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php',
'PhabricatorCalendarExport' => 'applications/calendar/storage/PhabricatorCalendarExport.php',
'PhabricatorCalendarExportDisableController' => 'applications/calendar/controller/PhabricatorCalendarExportDisableController.php',
'PhabricatorCalendarExportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php',
'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php',
'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php',
'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php',
'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php',
'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php',
'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php',
'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php',
'PhabricatorCalendarExportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarExportPHIDType.php',
'PhabricatorCalendarExportQuery' => 'applications/calendar/query/PhabricatorCalendarExportQuery.php',
'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php',
'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php',
'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php',
'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php',
'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php',
'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php',
'PhabricatorCalendarExternalInvitee' => 'applications/calendar/storage/PhabricatorCalendarExternalInvitee.php',
'PhabricatorCalendarExternalInviteePHIDType' => 'applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php',
'PhabricatorCalendarExternalInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php',
'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php',
'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php',
'PhabricatorCalendarICSURIImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php',
'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php',
'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php',
'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php',
'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php',
'PhabricatorCalendarImportDeleteController' => 'applications/calendar/controller/PhabricatorCalendarImportDeleteController.php',
'PhabricatorCalendarImportDeleteLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDeleteLogType.php',
'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php',
'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php',
'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php',
'PhabricatorCalendarImportDropController' => 'applications/calendar/controller/PhabricatorCalendarImportDropController.php',
'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php',
'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php',
'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php',
'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php',
'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php',
'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php',
'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php',
'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php',
'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php',
'PhabricatorCalendarImportFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php',
'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php',
'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php',
'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php',
'PhabricatorCalendarImportICSWarningLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSWarningLogType.php',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php',
'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php',
'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php',
'PhabricatorCalendarImportLogListController' => 'applications/calendar/controller/PhabricatorCalendarImportLogListController.php',
'PhabricatorCalendarImportLogQuery' => 'applications/calendar/query/PhabricatorCalendarImportLogQuery.php',
'PhabricatorCalendarImportLogSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php',
'PhabricatorCalendarImportLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportLogType.php',
'PhabricatorCalendarImportLogView' => 'applications/calendar/view/PhabricatorCalendarImportLogView.php',
'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php',
'PhabricatorCalendarImportOriginalLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php',
'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php',
'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php',
'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php',
'PhabricatorCalendarImportQueueLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportQueueLogType.php',
'PhabricatorCalendarImportReloadController' => 'applications/calendar/controller/PhabricatorCalendarImportReloadController.php',
'PhabricatorCalendarImportReloadTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php',
'PhabricatorCalendarImportReloadWorker' => 'applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php',
'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php',
'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php',
'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php',
'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php',
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
'PhabricatorCalendarInviteeDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php',
'PhabricatorCalendarInviteeUserDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php',
'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php',
'PhabricatorCalendarManagementReloadWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementReloadWorkflow.php',
'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php',
'PhabricatorCalendarNotification' => 'applications/calendar/storage/PhabricatorCalendarNotification.php',
'PhabricatorCalendarNotificationEngine' => 'applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php',
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
'PhabricatorChatLogChannelQuery' => 'applications/chatlog/query/PhabricatorChatLogChannelQuery.php',
'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php',
'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php',
'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php',
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
+ 'PhabricatorCheckboxesEditField' => 'applications/transactions/editfield/PhabricatorCheckboxesEditField.php',
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
'PhabricatorClassConfigType' => 'applications/config/type/PhabricatorClassConfigType.php',
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
'PhabricatorClusterDatabasesConfigType' => 'infrastructure/cluster/config/PhabricatorClusterDatabasesConfigType.php',
'PhabricatorClusterException' => 'infrastructure/cluster/exception/PhabricatorClusterException.php',
'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php',
'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php',
'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php',
'PhabricatorClusterMailersConfigType' => 'infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php',
'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php',
'PhabricatorClusterSearchConfigType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigType.php',
'PhabricatorClusterServiceHealthRecord' => 'infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php',
'PhabricatorClusterStrandedException' => 'infrastructure/cluster/exception/PhabricatorClusterStrandedException.php',
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php',
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php',
'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php',
'PhabricatorCommitBranchesField' => 'applications/repository/customfield/PhabricatorCommitBranchesField.php',
'PhabricatorCommitCustomField' => 'applications/repository/customfield/PhabricatorCommitCustomField.php',
'PhabricatorCommitMergedCommitsField' => 'applications/repository/customfield/PhabricatorCommitMergedCommitsField.php',
'PhabricatorCommitRepositoryField' => 'applications/repository/customfield/PhabricatorCommitRepositoryField.php',
'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php',
'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php',
'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php',
'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php',
'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php',
'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php',
'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php',
'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php',
'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php',
'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php',
'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php',
'PhabricatorConduitEditField' => 'applications/transactions/editfield/PhabricatorConduitEditField.php',
'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php',
'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php',
'PhabricatorConduitLogSearchEngine' => 'applications/conduit/query/PhabricatorConduitLogSearchEngine.php',
'PhabricatorConduitManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitManagementWorkflow.php',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php',
'PhabricatorConduitResultInterface' => 'applications/conduit/interface/PhabricatorConduitResultInterface.php',
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
'PhabricatorConduitSearchFieldSpecification' => 'applications/conduit/interface/PhabricatorConduitSearchFieldSpecification.php',
'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php',
'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php',
'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php',
'PhabricatorConduitTokensSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php',
'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
'PhabricatorConfigApplicationController' => 'applications/config/controller/PhabricatorConfigApplicationController.php',
'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php',
'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php',
'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php',
'PhabricatorConfigClusterRepositoriesController' => 'applications/config/controller/PhabricatorConfigClusterRepositoriesController.php',
'PhabricatorConfigClusterSearchController' => 'applications/config/controller/PhabricatorConfigClusterSearchController.php',
'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
'PhabricatorConfigConstants' => 'applications/config/constants/PhabricatorConfigConstants.php',
'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
'PhabricatorConfigCoreSchemaSpec' => 'applications/config/schema/PhabricatorConfigCoreSchemaSpec.php',
'PhabricatorConfigDatabaseController' => 'applications/config/controller/PhabricatorConfigDatabaseController.php',
'PhabricatorConfigDatabaseIssueController' => 'applications/config/controller/PhabricatorConfigDatabaseIssueController.php',
'PhabricatorConfigDatabaseSchema' => 'applications/config/schema/PhabricatorConfigDatabaseSchema.php',
'PhabricatorConfigDatabaseSource' => 'infrastructure/env/PhabricatorConfigDatabaseSource.php',
'PhabricatorConfigDatabaseStatusController' => 'applications/config/controller/PhabricatorConfigDatabaseStatusController.php',
'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php',
'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
'PhabricatorConfigEdgeModule' => 'applications/config/module/PhabricatorConfigEdgeModule.php',
'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php',
'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php',
'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
'PhabricatorConfigGroupConstants' => 'applications/config/constants/PhabricatorConfigGroupConstants.php',
'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php',
'PhabricatorConfigHTTPParameterTypesModule' => 'applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php',
'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php',
'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php',
'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
'PhabricatorConfigIssuePanelController' => 'applications/config/controller/PhabricatorConfigIssuePanelController.php',
'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php',
'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php',
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
'PhabricatorConfigManagementDoneWorkflow' => 'applications/config/management/PhabricatorConfigManagementDoneWorkflow.php',
'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php',
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
'PhabricatorConfigManualActivity' => 'applications/config/storage/PhabricatorConfigManualActivity.php',
'PhabricatorConfigModule' => 'applications/config/module/PhabricatorConfigModule.php',
'PhabricatorConfigModuleController' => 'applications/config/controller/PhabricatorConfigModuleController.php',
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
'PhabricatorConfigPHIDModule' => 'applications/config/module/PhabricatorConfigPHIDModule.php',
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
'PhabricatorConfigPurgeCacheController' => 'applications/config/controller/PhabricatorConfigPurgeCacheController.php',
'PhabricatorConfigRegexOptionType' => 'applications/config/custom/PhabricatorConfigRegexOptionType.php',
'PhabricatorConfigRequestExceptionHandlerModule' => 'applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php',
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php',
'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php',
'PhabricatorConfigSetupCheckModule' => 'applications/config/module/PhabricatorConfigSetupCheckModule.php',
'PhabricatorConfigSiteModule' => 'applications/config/module/PhabricatorConfigSiteModule.php',
'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php',
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
'PhabricatorConfigStorageSchema' => 'applications/config/schema/PhabricatorConfigStorageSchema.php',
'PhabricatorConfigTableSchema' => 'applications/config/schema/PhabricatorConfigTableSchema.php',
'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php',
'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php',
'PhabricatorConfigType' => 'applications/config/type/PhabricatorConfigType.php',
'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php',
'PhabricatorConfigVersionController' => 'applications/config/controller/PhabricatorConfigVersionController.php',
'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php',
'PhabricatorConpherenceColumnMinimizeSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnMinimizeSetting.php',
'PhabricatorConpherenceColumnVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnVisibleSetting.php',
'PhabricatorConpherenceNotificationsSetting' => 'applications/settings/setting/PhabricatorConpherenceNotificationsSetting.php',
'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php',
'PhabricatorConpherenceProfileMenuItem' => 'applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php',
'PhabricatorConpherenceRoomContextFreeGrammar' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomContextFreeGrammar.php',
'PhabricatorConpherenceRoomTestDataGenerator' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomTestDataGenerator.php',
'PhabricatorConpherenceSoundSetting' => 'applications/settings/setting/PhabricatorConpherenceSoundSetting.php',
'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php',
'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php',
'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php',
'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php',
'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php',
'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php',
'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php',
'PhabricatorContributedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorContributedToObjectEdgeType.php',
'PhabricatorController' => 'applications/base/controller/PhabricatorController.php',
'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php',
'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php',
'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php',
'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php',
'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php',
'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php',
'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php',
'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php',
'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php',
'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php',
'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php',
'PhabricatorCountdownDefaultEditCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php',
'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php',
'PhabricatorCountdownDescriptionTransaction' => 'applications/countdown/xaction/PhabricatorCountdownDescriptionTransaction.php',
'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php',
'PhabricatorCountdownEditEngine' => 'applications/countdown/editor/PhabricatorCountdownEditEngine.php',
'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.php',
'PhabricatorCountdownEpochTransaction' => 'applications/countdown/xaction/PhabricatorCountdownEpochTransaction.php',
'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php',
'PhabricatorCountdownMailReceiver' => 'applications/countdown/mail/PhabricatorCountdownMailReceiver.php',
'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php',
'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php',
'PhabricatorCountdownReplyHandler' => 'applications/countdown/mail/PhabricatorCountdownReplyHandler.php',
'PhabricatorCountdownSchemaSpec' => 'applications/countdown/storage/PhabricatorCountdownSchemaSpec.php',
'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php',
'PhabricatorCountdownTitleTransaction' => 'applications/countdown/xaction/PhabricatorCountdownTitleTransaction.php',
'PhabricatorCountdownTransaction' => 'applications/countdown/storage/PhabricatorCountdownTransaction.php',
'PhabricatorCountdownTransactionComment' => 'applications/countdown/storage/PhabricatorCountdownTransactionComment.php',
'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php',
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
'PhabricatorCustomFieldApplicationSearchDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php',
'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php',
'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php',
'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
'PhabricatorCustomFieldEditEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php',
'PhabricatorCustomFieldEditField' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php',
'PhabricatorCustomFieldEditType' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php',
'PhabricatorCustomFieldExportEngineExtension' => 'infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php',
'PhabricatorCustomFieldFulltextEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldFulltextEngineExtension.php',
'PhabricatorCustomFieldHeraldAction' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldAction.php',
'PhabricatorCustomFieldHeraldActionGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldActionGroup.php',
'PhabricatorCustomFieldHeraldField' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php',
'PhabricatorCustomFieldHeraldFieldGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldFieldGroup.php',
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
'PhabricatorCustomFieldMonogramParser' => 'infrastructure/customfield/parser/PhabricatorCustomFieldMonogramParser.php',
'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
'PhabricatorCustomFieldSearchEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldSearchEngineExtension.php',
'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
'PhabricatorCustomFieldStorageQuery' => 'infrastructure/customfield/query/PhabricatorCustomFieldStorageQuery.php',
'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
'PhabricatorCustomLogoConfigType' => 'applications/config/custom/PhabricatorCustomLogoConfigType.php',
'PhabricatorCustomUIFooterConfigType' => 'applications/config/custom/PhabricatorCustomUIFooterConfigType.php',
'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php',
'PhabricatorDaemonBulkJobController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobController.php',
'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php',
'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php',
'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php',
'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php',
'PhabricatorDaemonContentSource' => 'infrastructure/daemon/contentsource/PhabricatorDaemonContentSource.php',
'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php',
'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php',
'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.php',
'PhabricatorDaemonLockLog' => 'applications/daemon/storage/PhabricatorDaemonLockLog.php',
'PhabricatorDaemonLockLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php',
'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php',
'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php',
'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php',
'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php',
'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php',
'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php',
'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php',
'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php',
'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php',
'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php',
'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php',
'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php',
'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php',
'PhabricatorDaemonManagementLogWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php',
'PhabricatorDaemonManagementReloadWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php',
'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php',
'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php',
'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php',
'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php',
'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php',
'PhabricatorDaemonOverseerModule' => 'infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php',
'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php',
'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php',
'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php',
'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php',
'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php',
'PhabricatorDarkConsoleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleSetting.php',
'PhabricatorDarkConsoleTabSetting' => 'applications/settings/setting/PhabricatorDarkConsoleTabSetting.php',
'PhabricatorDarkConsoleVisibleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleVisibleSetting.php',
'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php',
'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardArchiveController.php',
'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php',
'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php',
'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php',
'PhabricatorDashboardDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardDatasource.php',
'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
'PhabricatorDashboardIconSet' => 'applications/dashboard/icon/PhabricatorDashboardIconSet.php',
'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php',
'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php',
'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php',
'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
'PhabricatorDashboardNgrams' => 'applications/dashboard/storage/PhabricatorDashboardNgrams.php',
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php',
'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
'PhabricatorDashboardPanelDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php',
'PhabricatorDashboardPanelEditConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPanelEditConduitAPIMethod.php',
'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
'PhabricatorDashboardPanelEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php',
'PhabricatorDashboardPanelEditproController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditproController.php',
'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php',
'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php',
'PhabricatorDashboardPanelNgrams' => 'applications/dashboard/storage/PhabricatorDashboardPanelNgrams.php',
'PhabricatorDashboardPanelPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPanelPHIDType.php',
'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php',
'PhabricatorDashboardPanelRenderController' => 'applications/dashboard/controller/PhabricatorDashboardPanelRenderController.php',
'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php',
'PhabricatorDashboardPanelSearchApplicationCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchApplicationCustomField.php',
'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php',
'PhabricatorDashboardPanelSearchQueryCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php',
'PhabricatorDashboardPanelTabsCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelTabsCustomField.php',
'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php',
'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php',
'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php',
'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php',
'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php',
'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php',
'PhabricatorDashboardProfileMenuItem' => 'applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php',
'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php',
'PhabricatorDashboardQueryPanelInstallController' => 'applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php',
'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php',
'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php',
'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php',
'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php',
'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php',
'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php',
'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php',
'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php',
'PhabricatorDashboardTransaction' => 'applications/dashboard/storage/PhabricatorDashboardTransaction.php',
'PhabricatorDashboardTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php',
'PhabricatorDashboardTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardTransactionQuery.php',
'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php',
'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php',
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
'PhabricatorDatasourceApplicationEngineExtension' => 'applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php',
'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php',
'PhabricatorDatasourceEditType' => 'applications/transactions/edittype/PhabricatorDatasourceEditType.php',
'PhabricatorDatasourceEngine' => 'applications/search/engine/PhabricatorDatasourceEngine.php',
'PhabricatorDatasourceEngineExtension' => 'applications/search/engineextension/PhabricatorDatasourceEngineExtension.php',
'PhabricatorDateFormatSetting' => 'applications/settings/setting/PhabricatorDateFormatSetting.php',
'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php',
'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php',
'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php',
'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php',
'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php',
'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php',
'PhabricatorDestructionEngineExtension' => 'applications/system/engine/PhabricatorDestructionEngineExtension.php',
'PhabricatorDestructionEngineExtensionModule' => 'applications/system/engine/PhabricatorDestructionEngineExtensionModule.php',
'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php',
'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php',
'PhabricatorDifferentialAttachCommitWorkflow' => 'applications/differential/management/PhabricatorDifferentialAttachCommitWorkflow.php',
'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php',
'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php',
+ 'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php',
'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php',
'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php',
'PhabricatorDisplayPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php',
'PhabricatorDisqusAuthProvider' => 'applications/auth/provider/PhabricatorDisqusAuthProvider.php',
'PhabricatorDividerEditField' => 'applications/transactions/editfield/PhabricatorDividerEditField.php',
'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php',
'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php',
'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php',
'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php',
'PhabricatorDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorDocumentRenderingEngine.php',
'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php',
+ 'PhabricatorDoubleExportField' => 'infrastructure/export/field/PhabricatorDoubleExportField.php',
'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php',
'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php',
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php',
'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php',
'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php',
'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php',
'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php',
'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php',
'PhabricatorEdgeObject' => 'infrastructure/edges/conduit/PhabricatorEdgeObject.php',
'PhabricatorEdgeObjectQuery' => 'infrastructure/edges/query/PhabricatorEdgeObjectQuery.php',
'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php',
'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php',
'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php',
'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php',
'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php',
'PhabricatorEditEngineBulkJobType' => 'applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php',
'PhabricatorEditEngineCheckboxesCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php',
'PhabricatorEditEngineColumnsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineColumnsCommentAction.php',
'PhabricatorEditEngineCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php',
'PhabricatorEditEngineCommentActionGroup' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentActionGroup.php',
'PhabricatorEditEngineConfiguration' => 'applications/transactions/storage/PhabricatorEditEngineConfiguration.php',
'PhabricatorEditEngineConfigurationDefaultCreateController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php',
'PhabricatorEditEngineConfigurationDefaultsController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php',
'PhabricatorEditEngineConfigurationDisableController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php',
'PhabricatorEditEngineConfigurationEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationEditController.php',
'PhabricatorEditEngineConfigurationEditEngine' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php',
'PhabricatorEditEngineConfigurationEditor' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php',
'PhabricatorEditEngineConfigurationIsEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php',
'PhabricatorEditEngineConfigurationListController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php',
'PhabricatorEditEngineConfigurationLockController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php',
'PhabricatorEditEngineConfigurationPHIDType' => 'applications/transactions/phid/PhabricatorEditEngineConfigurationPHIDType.php',
'PhabricatorEditEngineConfigurationQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationQuery.php',
'PhabricatorEditEngineConfigurationReorderController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php',
'PhabricatorEditEngineConfigurationSaveController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSaveController.php',
'PhabricatorEditEngineConfigurationSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php',
'PhabricatorEditEngineConfigurationSortController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php',
'PhabricatorEditEngineConfigurationSubtypeController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php',
'PhabricatorEditEngineConfigurationTransaction' => 'applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php',
'PhabricatorEditEngineConfigurationTransactionQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationTransactionQuery.php',
'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php',
'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php',
'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php',
'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php',
'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php',
'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php',
'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php',
'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php',
'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php',
'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php',
'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php',
'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php',
'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php',
'PhabricatorEditEngineSelectCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineSelectCommentAction.php',
'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php',
'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php',
'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php',
'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php',
'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php',
'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php',
'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php',
'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php',
'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php',
'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php',
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php',
'PhabricatorElasticsearchHost' => 'infrastructure/cluster/search/PhabricatorElasticsearchHost.php',
'PhabricatorElasticsearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php',
'PhabricatorElasticsearchSetupCheck' => 'applications/config/check/PhabricatorElasticsearchSetupCheck.php',
'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
'PhabricatorEmailContentSource' => 'applications/metamta/contentsource/PhabricatorEmailContentSource.php',
'PhabricatorEmailDeliverySettingsPanel' => 'applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php',
'PhabricatorEmailFormatSetting' => 'applications/settings/setting/PhabricatorEmailFormatSetting.php',
'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php',
'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php',
'PhabricatorEmailNotificationsSetting' => 'applications/settings/setting/PhabricatorEmailNotificationsSetting.php',
'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php',
'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php',
'PhabricatorEmailSelfActionsSetting' => 'applications/settings/setting/PhabricatorEmailSelfActionsSetting.php',
'PhabricatorEmailStampsSetting' => 'applications/settings/setting/PhabricatorEmailStampsSetting.php',
'PhabricatorEmailTagsSetting' => 'applications/settings/setting/PhabricatorEmailTagsSetting.php',
'PhabricatorEmailVarySubjectsSetting' => 'applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php',
'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php',
'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php',
'PhabricatorEmojiDatasource' => 'applications/macro/typeahead/PhabricatorEmojiDatasource.php',
'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php',
'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php',
'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php',
'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php',
'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php',
'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php',
'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php',
'PhabricatorEpochExportField' => 'infrastructure/export/field/PhabricatorEpochExportField.php',
'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php',
'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php',
'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php',
'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php',
'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php',
'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php',
'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php',
'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php',
'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php',
'PhabricatorExportField' => 'infrastructure/export/field/PhabricatorExportField.php',
'PhabricatorExportFormat' => 'infrastructure/export/format/PhabricatorExportFormat.php',
'PhabricatorExportFormatSetting' => 'infrastructure/export/engine/PhabricatorExportFormatSetting.php',
'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php',
'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php',
'PhabricatorExternalAccountQuery' => 'applications/auth/query/PhabricatorExternalAccountQuery.php',
'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php',
'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php',
'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php',
'PhabricatorFact' => 'applications/fact/fact/PhabricatorFact.php',
'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php',
'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php',
'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php',
'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php',
'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php',
'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php',
'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php',
'PhabricatorFactDatapointQuery' => 'applications/fact/query/PhabricatorFactDatapointQuery.php',
'PhabricatorFactDimension' => 'applications/fact/storage/PhabricatorFactDimension.php',
'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php',
'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php',
'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php',
'PhabricatorFactIntDatapoint' => 'applications/fact/storage/PhabricatorFactIntDatapoint.php',
'PhabricatorFactKeyDimension' => 'applications/fact/storage/PhabricatorFactKeyDimension.php',
'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php',
'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php',
'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php',
'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php',
'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php',
'PhabricatorFactObjectController' => 'applications/fact/controller/PhabricatorFactObjectController.php',
'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php',
'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php',
'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php',
'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php',
'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php',
'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php',
'PhabricatorFavoritesMenuItemController' => 'applications/favorites/controller/PhabricatorFavoritesMenuItemController.php',
'PhabricatorFavoritesProfileMenuEngine' => 'applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php',
'PhabricatorFaxContentSource' => 'infrastructure/contentsource/PhabricatorFaxContentSource.php',
'PhabricatorFeedApplication' => 'applications/feed/application/PhabricatorFeedApplication.php',
'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php',
'PhabricatorFeedConfigOptions' => 'applications/feed/config/PhabricatorFeedConfigOptions.php',
'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php',
'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php',
'PhabricatorFeedDetailController' => 'applications/feed/controller/PhabricatorFeedDetailController.php',
'PhabricatorFeedListController' => 'applications/feed/controller/PhabricatorFeedListController.php',
'PhabricatorFeedManagementRepublishWorkflow' => 'applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php',
'PhabricatorFeedManagementWorkflow' => 'applications/feed/management/PhabricatorFeedManagementWorkflow.php',
'PhabricatorFeedQuery' => 'applications/feed/query/PhabricatorFeedQuery.php',
'PhabricatorFeedSearchEngine' => 'applications/feed/query/PhabricatorFeedSearchEngine.php',
'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php',
'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php',
'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php',
'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
'PhabricatorFerretEngine' => 'applications/search/ferret/PhabricatorFerretEngine.php',
'PhabricatorFerretEngineTestCase' => 'applications/search/ferret/__tests__/PhabricatorFerretEngineTestCase.php',
'PhabricatorFerretFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php',
'PhabricatorFerretFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php',
'PhabricatorFerretInterface' => 'applications/search/ferret/PhabricatorFerretInterface.php',
'PhabricatorFerretMetadata' => 'applications/search/ferret/PhabricatorFerretMetadata.php',
'PhabricatorFerretSearchEngineExtension' => 'applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php',
'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
'PhabricatorFileAES256StorageFormat' => 'applications/files/format/PhabricatorFileAES256StorageFormat.php',
'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php',
'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php',
'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php',
'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php',
'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php',
'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php',
'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php',
'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php',
'PhabricatorFileDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php',
'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php',
'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php',
'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php',
'PhabricatorFileEditField' => 'applications/transactions/editfield/PhabricatorFileEditField.php',
'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php',
'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php',
'PhabricatorFileExternalRequestGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php',
'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php',
'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php',
'PhabricatorFileIconSetSelectController' => 'applications/files/controller/PhabricatorFileIconSetSelectController.php',
'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php',
'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php',
'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php',
'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php',
'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
'PhabricatorFileNameNgrams' => 'applications/files/storage/PhabricatorFileNameNgrams.php',
'PhabricatorFileNameTransaction' => 'applications/files/xaction/PhabricatorFileNameTransaction.php',
'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php',
'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php',
'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php',
'PhabricatorFileSearchConduitAPIMethod' => 'applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php',
'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php',
'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php',
'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php',
'PhabricatorFileStorageEngineTestCase' => 'applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php',
'PhabricatorFileStorageFormat' => 'applications/files/format/PhabricatorFileStorageFormat.php',
'PhabricatorFileStorageFormatTestCase' => 'applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php',
'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php',
'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php',
'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php',
'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php',
'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php',
'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php',
'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php',
'PhabricatorFileTransactionType' => 'applications/files/xaction/PhabricatorFileTransactionType.php',
'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php',
'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php',
'PhabricatorFileTransformTestCase' => 'applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php',
'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php',
'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php',
'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php',
'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php',
'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php',
'PhabricatorFileViewController' => 'applications/files/controller/PhabricatorFileViewController.php',
'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php',
'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php',
'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php',
'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php',
'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php',
'PhabricatorFilesManagementCycleWorkflow' => 'applications/files/management/PhabricatorFilesManagementCycleWorkflow.php',
'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php',
'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php',
'PhabricatorFilesManagementIntegrityWorkflow' => 'applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php',
'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php',
'PhabricatorFilesOnDiskBuiltinFile' => 'applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php',
'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php',
'PhabricatorFiletreeVisibleSetting' => 'applications/settings/setting/PhabricatorFiletreeVisibleSetting.php',
'PhabricatorFiletreeWidthSetting' => 'applications/settings/setting/PhabricatorFiletreeWidthSetting.php',
'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php',
'PhabricatorFlagAddFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php',
'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php',
'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php',
'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php',
'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php',
'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php',
'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php',
'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php',
'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php',
'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php',
'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php',
'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php',
'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php',
'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php',
'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php',
'PhabricatorFulltextEngine' => 'applications/search/index/PhabricatorFulltextEngine.php',
'PhabricatorFulltextEngineExtension' => 'applications/search/index/PhabricatorFulltextEngineExtension.php',
'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php',
'PhabricatorFulltextIndexEngineExtension' => 'applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php',
'PhabricatorFulltextInterface' => 'applications/search/interface/PhabricatorFulltextInterface.php',
'PhabricatorFulltextResultSet' => 'applications/search/query/PhabricatorFulltextResultSet.php',
'PhabricatorFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php',
'PhabricatorFulltextToken' => 'applications/search/query/PhabricatorFulltextToken.php',
'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php',
'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php',
'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php',
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCollectWorkflow.php',
'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementSetPolicyWorkflow.php',
'PhabricatorGarbageCollectorManagementWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementWorkflow.php',
'PhabricatorGeneralCachePurger' => 'applications/cache/purger/PhabricatorGeneralCachePurger.php',
'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php',
'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php',
'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php',
'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
'PhabricatorGuidanceContext' => 'applications/guides/guidance/PhabricatorGuidanceContext.php',
'PhabricatorGuidanceEngine' => 'applications/guides/guidance/PhabricatorGuidanceEngine.php',
'PhabricatorGuidanceEngineExtension' => 'applications/guides/guidance/PhabricatorGuidanceEngineExtension.php',
'PhabricatorGuidanceMessage' => 'applications/guides/guidance/PhabricatorGuidanceMessage.php',
'PhabricatorGuideApplication' => 'applications/guides/application/PhabricatorGuideApplication.php',
'PhabricatorGuideController' => 'applications/guides/controller/PhabricatorGuideController.php',
'PhabricatorGuideInstallModule' => 'applications/guides/module/PhabricatorGuideInstallModule.php',
'PhabricatorGuideItemView' => 'applications/guides/view/PhabricatorGuideItemView.php',
'PhabricatorGuideListView' => 'applications/guides/view/PhabricatorGuideListView.php',
'PhabricatorGuideModule' => 'applications/guides/module/PhabricatorGuideModule.php',
'PhabricatorGuideModuleController' => 'applications/guides/controller/PhabricatorGuideModuleController.php',
'PhabricatorGuideQuickStartModule' => 'applications/guides/module/PhabricatorGuideQuickStartModule.php',
'PhabricatorHMACTestCase' => 'infrastructure/util/__tests__/PhabricatorHMACTestCase.php',
'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php',
'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php',
'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
'PhabricatorHandleRemarkupRule' => 'applications/phid/remarkup/PhabricatorHandleRemarkupRule.php',
'PhabricatorHandlesEditField' => 'applications/transactions/editfield/PhabricatorHandlesEditField.php',
'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php',
'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php',
'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php',
'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php',
'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php',
'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php',
'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php',
'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php',
'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php',
'PhabricatorHomeLauncherProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php',
'PhabricatorHomeMenuItemController' => 'applications/home/controller/PhabricatorHomeMenuItemController.php',
'PhabricatorHomeProfileMenuEngine' => 'applications/home/engine/PhabricatorHomeProfileMenuEngine.php',
'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php',
'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php',
'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php',
'PhabricatorIDExportField' => 'infrastructure/export/field/PhabricatorIDExportField.php',
'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php',
'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php',
'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php',
'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php',
'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php',
'PhabricatorIconSetEditField' => 'applications/transactions/editfield/PhabricatorIconSetEditField.php',
'PhabricatorIconSetIcon' => 'applications/files/iconset/PhabricatorIconSetIcon.php',
'PhabricatorImageDocumentEngine' => 'applications/files/document/PhabricatorImageDocumentEngine.php',
'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php',
'PhabricatorImageRemarkupRule' => 'applications/files/markup/PhabricatorImageRemarkupRule.php',
'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php',
'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.php',
'PhabricatorInFlightErrorView' => 'applications/config/view/PhabricatorInFlightErrorView.php',
'PhabricatorIndexEngine' => 'applications/search/index/PhabricatorIndexEngine.php',
'PhabricatorIndexEngineExtension' => 'applications/search/index/PhabricatorIndexEngineExtension.php',
'PhabricatorIndexEngineExtensionModule' => 'applications/search/index/PhabricatorIndexEngineExtensionModule.php',
'PhabricatorIndexableInterface' => 'applications/search/interface/PhabricatorIndexableInterface.php',
'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php',
'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php',
'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php',
'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php',
'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php',
'PhabricatorIntEditField' => 'applications/transactions/editfield/PhabricatorIntEditField.php',
'PhabricatorIntExportField' => 'infrastructure/export/field/PhabricatorIntExportField.php',
'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php',
'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php',
'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php',
'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php',
'PhabricatorJSONDocumentEngine' => 'applications/files/document/PhabricatorJSONDocumentEngine.php',
'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php',
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
'PhabricatorJupyterDocumentEngine' => 'applications/files/document/PhabricatorJupyterDocumentEngine.php',
'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
'PhabricatorKeyValueSerializingCacheProxy' => 'applications/cache/PhabricatorKeyValueSerializingCacheProxy.php',
'PhabricatorKeyboardRemarkupRule' => 'infrastructure/markup/rule/PhabricatorKeyboardRemarkupRule.php',
'PhabricatorKeyring' => 'applications/files/keyring/PhabricatorKeyring.php',
'PhabricatorKeyringConfigOptionType' => 'applications/files/keyring/PhabricatorKeyringConfigOptionType.php',
'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php',
'PhabricatorLabelProfileMenuItem' => 'applications/search/menuitem/PhabricatorLabelProfileMenuItem.php',
'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php',
'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php',
'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php',
'PhabricatorLegalpadSignaturePolicyRule' => 'applications/legalpad/policyrule/PhabricatorLegalpadSignaturePolicyRule.php',
'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php',
'PhabricatorLinkProfileMenuItem' => 'applications/search/menuitem/PhabricatorLinkProfileMenuItem.php',
'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php',
'PhabricatorLipsumContentSource' => 'infrastructure/contentsource/PhabricatorLipsumContentSource.php',
'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php',
'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
'PhabricatorLiskExportEngineExtension' => 'infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php',
'PhabricatorLiskFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorLiskFulltextEngineExtension.php',
'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php',
'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
'PhabricatorListExportField' => 'infrastructure/export/field/PhabricatorListExportField.php',
'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php',
'PhabricatorLocaleScopeGuardTestCase' => 'infrastructure/internationalization/scope/__tests__/PhabricatorLocaleScopeGuardTestCase.php',
'PhabricatorLockLogManagementWorkflow' => 'applications/daemon/management/PhabricatorLockLogManagementWorkflow.php',
'PhabricatorLockManagementWorkflow' => 'applications/daemon/management/PhabricatorLockManagementWorkflow.php',
'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php',
'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php',
'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php',
'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php',
'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php',
'PhabricatorMacroAudioTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioTransaction.php',
'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php',
'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php',
'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php',
'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php',
'PhabricatorMacroDisabledTransaction' => 'applications/macro/xaction/PhabricatorMacroDisabledTransaction.php',
'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php',
'PhabricatorMacroEditEngine' => 'applications/macro/editor/PhabricatorMacroEditEngine.php',
'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php',
'PhabricatorMacroFileTransaction' => 'applications/macro/xaction/PhabricatorMacroFileTransaction.php',
'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php',
'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php',
'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php',
'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php',
'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php',
'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php',
'PhabricatorMacroNameTransaction' => 'applications/macro/xaction/PhabricatorMacroNameTransaction.php',
'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php',
'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php',
'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php',
'PhabricatorMacroTestCase' => 'applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php',
'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php',
'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php',
'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php',
'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php',
'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php',
'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php',
'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php',
'PhabricatorMailEmailSubjectHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailSubjectHeraldField.php',
'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php',
'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php',
'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php',
'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php',
'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php',
'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php',
'PhabricatorMailImplementationPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php',
'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php',
'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php',
'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php',
'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php',
'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php',
'PhabricatorMailManagementResendWorkflow' => 'applications/metamta/management/PhabricatorMailManagementResendWorkflow.php',
'PhabricatorMailManagementSendTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php',
'PhabricatorMailManagementShowInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php',
'PhabricatorMailManagementShowOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php',
'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php',
'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php',
'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php',
'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php',
'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php',
'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php',
'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php',
'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php',
'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php',
+ 'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php',
'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php',
'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php',
'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php',
'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php',
'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php',
'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php',
'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php',
'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php',
'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php',
'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php',
'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php',
'PhabricatorManageProfileMenuItem' => 'applications/search/menuitem/PhabricatorManageProfileMenuItem.php',
'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php',
'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php',
'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php',
'PhabricatorManiphestTaskFactEngine' => 'applications/fact/engine/PhabricatorManiphestTaskFactEngine.php',
'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
'PhabricatorManualActivitySetupCheck' => 'applications/config/check/PhabricatorManualActivitySetupCheck.php',
'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
'PhabricatorMarkupEngineTestCase' => 'infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php',
'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php',
'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php',
'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php',
'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php',
'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php',
'PhabricatorMentionableInterface' => 'applications/transactions/interface/PhabricatorMentionableInterface.php',
'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php',
'PhabricatorMetaMTAActor' => 'applications/metamta/query/PhabricatorMetaMTAActor.php',
'PhabricatorMetaMTAActorQuery' => 'applications/metamta/query/PhabricatorMetaMTAActorQuery.php',
'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php',
'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php',
'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php',
'PhabricatorMetaMTAApplicationEmailEditor' => 'applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php',
'PhabricatorMetaMTAApplicationEmailHeraldField' => 'applications/metamta/herald/PhabricatorMetaMTAApplicationEmailHeraldField.php',
'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php',
'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php',
'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php',
'PhabricatorMetaMTAApplicationEmailTransaction' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php',
'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php',
'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php',
'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php',
'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php',
'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php',
'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php',
'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php',
'PhabricatorMetaMTAEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php',
'PhabricatorMetaMTAEmailOthersHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php',
'PhabricatorMetaMTAEmailSelfHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php',
'PhabricatorMetaMTAErrorMailAction' => 'applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php',
'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'applications/metamta/edge/PhabricatorMetaMTAMailHasRecipientEdgeType.php',
'PhabricatorMetaMTAMailListController' => 'applications/metamta/controller/PhabricatorMetaMTAMailListController.php',
'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php',
+ 'PhabricatorMetaMTAMailProperties' => 'applications/metamta/storage/PhabricatorMetaMTAMailProperties.php',
+ 'PhabricatorMetaMTAMailPropertiesQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php',
'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php',
'PhabricatorMetaMTAMailSearchEngine' => 'applications/metamta/query/PhabricatorMetaMTAMailSearchEngine.php',
'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
'PhabricatorMetaMTAMailViewController' => 'applications/metamta/controller/PhabricatorMetaMTAMailViewController.php',
'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php',
'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php',
'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php',
'PhabricatorMetaMTAPostmarkReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php',
'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php',
'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php',
'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php',
'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php',
'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php',
'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php',
'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php',
'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php',
'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php',
'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php',
'PhabricatorMonospacedFontSetting' => 'applications/settings/setting/PhabricatorMonospacedFontSetting.php',
'PhabricatorMonospacedTextareasSetting' => 'applications/settings/setting/PhabricatorMonospacedTextareasSetting.php',
'PhabricatorMotivatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php',
'PhabricatorMultiColumnUIExample' => 'applications/uiexample/examples/PhabricatorMultiColumnUIExample.php',
'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php',
'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php',
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php',
'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php',
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
'PhabricatorNamedQueryConfig' => 'applications/search/storage/PhabricatorNamedQueryConfig.php',
'PhabricatorNamedQueryConfigQuery' => 'applications/search/query/PhabricatorNamedQueryConfigQuery.php',
'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php',
'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php',
'PhabricatorNgramsIndexEngineExtension' => 'applications/search/engineextension/PhabricatorNgramsIndexEngineExtension.php',
'PhabricatorNgramsInterface' => 'applications/search/interface/PhabricatorNgramsInterface.php',
'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php',
'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php',
'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php',
'PhabricatorNotificationConfigOptions' => 'applications/config/option/PhabricatorNotificationConfigOptions.php',
'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php',
'PhabricatorNotificationDestructionEngineExtension' => 'applications/notification/engineextension/PhabricatorNotificationDestructionEngineExtension.php',
'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php',
'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php',
'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php',
'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php',
'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php',
'PhabricatorNotificationServerRef' => 'applications/notification/client/PhabricatorNotificationServerRef.php',
'PhabricatorNotificationServersConfigType' => 'applications/notification/config/PhabricatorNotificationServersConfigType.php',
'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php',
'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php',
'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php',
'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php',
'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php',
'PhabricatorNotificationsSetting' => 'applications/settings/setting/PhabricatorNotificationsSetting.php',
'PhabricatorNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorNotificationsSettingsPanel.php',
'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php',
'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.php',
'PhabricatorOAuth1SecretTemporaryTokenType' => 'applications/auth/provider/PhabricatorOAuth1SecretTemporaryTokenType.php',
'PhabricatorOAuth2AuthProvider' => 'applications/auth/provider/PhabricatorOAuth2AuthProvider.php',
'PhabricatorOAuthAuthProvider' => 'applications/auth/provider/PhabricatorOAuthAuthProvider.php',
'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php',
'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php',
'PhabricatorOAuthClientController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientController.php',
'PhabricatorOAuthClientDisableController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDisableController.php',
'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php',
'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php',
'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php',
'PhabricatorOAuthClientTestController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientTestController.php',
'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php',
'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php',
'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php',
'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php',
'PhabricatorOAuthServerApplication' => 'applications/oauthserver/application/PhabricatorOAuthServerApplication.php',
'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php',
'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php',
'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php',
'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php',
'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientAuthorizationPHIDType.php',
'PhabricatorOAuthServerClientPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php',
'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php',
'PhabricatorOAuthServerClientSearchEngine' => 'applications/oauthserver/query/PhabricatorOAuthServerClientSearchEngine.php',
'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php',
'PhabricatorOAuthServerCreateClientsCapability' => 'applications/oauthserver/capability/PhabricatorOAuthServerCreateClientsCapability.php',
'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php',
'PhabricatorOAuthServerEditEngine' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditEngine.php',
'PhabricatorOAuthServerEditor' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditor.php',
'PhabricatorOAuthServerSchemaSpec' => 'applications/oauthserver/query/PhabricatorOAuthServerSchemaSpec.php',
'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php',
'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
'PhabricatorOAuthServerTransaction' => 'applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php',
'PhabricatorOAuthServerTransactionQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerTransactionQuery.php',
'PhabricatorObjectGraph' => 'infrastructure/graph/PhabricatorObjectGraph.php',
'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php',
'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php',
'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php',
'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.php',
'PhabricatorObjectHasDraftEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasDraftEdgeType.php',
'PhabricatorObjectHasFileEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasFileEdgeType.php',
'PhabricatorObjectHasJiraIssueEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasJiraIssueEdgeType.php',
'PhabricatorObjectHasSubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasSubscriberEdgeType.php',
'PhabricatorObjectHasUnsubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasUnsubscriberEdgeType.php',
'PhabricatorObjectHasWatcherEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasWatcherEdgeType.php',
'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php',
'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php',
'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php',
'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php',
'PhabricatorObjectMentionedByObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php',
'PhabricatorObjectMentionsObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php',
'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php',
'PhabricatorObjectRelationship' => 'applications/search/relationship/PhabricatorObjectRelationship.php',
'PhabricatorObjectRelationshipList' => 'applications/search/relationship/PhabricatorObjectRelationshipList.php',
'PhabricatorObjectRelationshipSource' => 'applications/search/relationship/PhabricatorObjectRelationshipSource.php',
'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php',
'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php',
'PhabricatorObjectStatus' => 'infrastructure/status/PhabricatorObjectStatus.php',
'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php',
'PhabricatorOldWorldContentSource' => 'infrastructure/contentsource/PhabricatorOldWorldContentSource.php',
'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php',
'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php',
'PhabricatorOptionGroupSetting' => 'applications/settings/setting/PhabricatorOptionGroupSetting.php',
'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php',
'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php',
'PhabricatorOwnersArchiveController' => 'applications/owners/controller/PhabricatorOwnersArchiveController.php',
'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php',
'PhabricatorOwnersConfiguredCustomField' => 'applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php',
'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php',
'PhabricatorOwnersCustomField' => 'applications/owners/customfield/PhabricatorOwnersCustomField.php',
'PhabricatorOwnersCustomFieldNumericIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldNumericIndex.php',
'PhabricatorOwnersCustomFieldStorage' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStorage.php',
'PhabricatorOwnersCustomFieldStringIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStringIndex.php',
'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php',
'PhabricatorOwnersDefaultEditCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultEditCapability.php',
'PhabricatorOwnersDefaultViewCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultViewCapability.php',
'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php',
'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php',
'PhabricatorOwnersHovercardEngineExtension' => 'applications/owners/engineextension/PhabricatorOwnersHovercardEngineExtension.php',
'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php',
'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php',
'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php',
'PhabricatorOwnersPackageAuditingTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php',
'PhabricatorOwnersPackageAutoreviewTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php',
'PhabricatorOwnersPackageContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPackageContextFreeGrammar.php',
'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php',
'PhabricatorOwnersPackageDescriptionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php',
'PhabricatorOwnersPackageDominionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php',
'PhabricatorOwnersPackageEditEngine' => 'applications/owners/editor/PhabricatorOwnersPackageEditEngine.php',
'PhabricatorOwnersPackageFerretEngine' => 'applications/owners/search/PhabricatorOwnersPackageFerretEngine.php',
'PhabricatorOwnersPackageFulltextEngine' => 'applications/owners/search/PhabricatorOwnersPackageFulltextEngine.php',
'PhabricatorOwnersPackageFunctionDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageFunctionDatasource.php',
+ 'PhabricatorOwnersPackageIgnoredTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php',
'PhabricatorOwnersPackageNameNgrams' => 'applications/owners/storage/PhabricatorOwnersPackageNameNgrams.php',
'PhabricatorOwnersPackageNameTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php',
'PhabricatorOwnersPackageOwnerDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageOwnerDatasource.php',
'PhabricatorOwnersPackageOwnersTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php',
'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php',
'PhabricatorOwnersPackagePathsTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php',
'PhabricatorOwnersPackagePrimaryTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php',
'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php',
'PhabricatorOwnersPackageRemarkupRule' => 'applications/owners/remarkup/PhabricatorOwnersPackageRemarkupRule.php',
'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php',
'PhabricatorOwnersPackageStatusTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageStatusTransaction.php',
'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php',
'PhabricatorOwnersPackageTestDataGenerator' => 'applications/owners/lipsum/PhabricatorOwnersPackageTestDataGenerator.php',
'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php',
'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php',
'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php',
'PhabricatorOwnersPackageTransactionType' => 'applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php',
'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php',
'PhabricatorOwnersPathContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPathContextFreeGrammar.php',
'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php',
'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php',
'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php',
'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php',
'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php',
'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
'PhabricatorPHIDExportField' => 'infrastructure/export/field/PhabricatorPHIDExportField.php',
'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php',
'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php',
'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php',
'PhabricatorPHIDListExportField' => 'infrastructure/export/field/PhabricatorPHIDListExportField.php',
'PhabricatorPHIDMailStamp' => 'applications/metamta/stamp/PhabricatorPHIDMailStamp.php',
'PhabricatorPHIDResolver' => 'applications/phid/resolver/PhabricatorPHIDResolver.php',
'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php',
'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php',
'PhabricatorPHIDsSearchField' => 'applications/search/field/PhabricatorPHIDsSearchField.php',
'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php',
'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php',
'PhabricatorPHPMailerConfigOptions' => 'applications/config/option/PhabricatorPHPMailerConfigOptions.php',
'PhabricatorPHPPreflightSetupCheck' => 'applications/config/check/PhabricatorPHPPreflightSetupCheck.php',
'PhabricatorPackagesApplication' => 'applications/packages/application/PhabricatorPackagesApplication.php',
'PhabricatorPackagesController' => 'applications/packages/controller/PhabricatorPackagesController.php',
'PhabricatorPackagesCreatePublisherCapability' => 'applications/packages/capability/PhabricatorPackagesCreatePublisherCapability.php',
'PhabricatorPackagesDAO' => 'applications/packages/storage/PhabricatorPackagesDAO.php',
'PhabricatorPackagesEditEngine' => 'applications/packages/editor/PhabricatorPackagesEditEngine.php',
'PhabricatorPackagesEditor' => 'applications/packages/editor/PhabricatorPackagesEditor.php',
'PhabricatorPackagesNgrams' => 'applications/packages/storage/PhabricatorPackagesNgrams.php',
'PhabricatorPackagesPackage' => 'applications/packages/storage/PhabricatorPackagesPackage.php',
'PhabricatorPackagesPackageController' => 'applications/packages/controller/PhabricatorPackagesPackageController.php',
'PhabricatorPackagesPackageDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPackageDatasource.php',
'PhabricatorPackagesPackageDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultEditCapability.php',
'PhabricatorPackagesPackageDefaultViewCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultViewCapability.php',
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageEditConduitAPIMethod.php',
'PhabricatorPackagesPackageEditController' => 'applications/packages/controller/PhabricatorPackagesPackageEditController.php',
'PhabricatorPackagesPackageEditEngine' => 'applications/packages/editor/PhabricatorPackagesPackageEditEngine.php',
'PhabricatorPackagesPackageEditor' => 'applications/packages/editor/PhabricatorPackagesPackageEditor.php',
'PhabricatorPackagesPackageKeyTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageKeyTransaction.php',
'PhabricatorPackagesPackageListController' => 'applications/packages/controller/PhabricatorPackagesPackageListController.php',
'PhabricatorPackagesPackageListView' => 'applications/packages/view/PhabricatorPackagesPackageListView.php',
'PhabricatorPackagesPackageNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPackageNameNgrams.php',
'PhabricatorPackagesPackageNameTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageNameTransaction.php',
'PhabricatorPackagesPackagePHIDType' => 'applications/packages/phid/PhabricatorPackagesPackagePHIDType.php',
'PhabricatorPackagesPackagePublisherTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackagePublisherTransaction.php',
'PhabricatorPackagesPackageQuery' => 'applications/packages/query/PhabricatorPackagesPackageQuery.php',
'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageSearchConduitAPIMethod.php',
'PhabricatorPackagesPackageSearchEngine' => 'applications/packages/query/PhabricatorPackagesPackageSearchEngine.php',
'PhabricatorPackagesPackageTransaction' => 'applications/packages/storage/PhabricatorPackagesPackageTransaction.php',
'PhabricatorPackagesPackageTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPackageTransactionQuery.php',
'PhabricatorPackagesPackageTransactionType' => 'applications/packages/xaction/package/PhabricatorPackagesPackageTransactionType.php',
'PhabricatorPackagesPackageViewController' => 'applications/packages/controller/PhabricatorPackagesPackageViewController.php',
'PhabricatorPackagesPublisher' => 'applications/packages/storage/PhabricatorPackagesPublisher.php',
'PhabricatorPackagesPublisherController' => 'applications/packages/controller/PhabricatorPackagesPublisherController.php',
'PhabricatorPackagesPublisherDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPublisherDatasource.php',
'PhabricatorPackagesPublisherDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPublisherDefaultEditCapability.php',
'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherEditConduitAPIMethod.php',
'PhabricatorPackagesPublisherEditController' => 'applications/packages/controller/PhabricatorPackagesPublisherEditController.php',
'PhabricatorPackagesPublisherEditEngine' => 'applications/packages/editor/PhabricatorPackagesPublisherEditEngine.php',
'PhabricatorPackagesPublisherEditor' => 'applications/packages/editor/PhabricatorPackagesPublisherEditor.php',
'PhabricatorPackagesPublisherKeyTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherKeyTransaction.php',
'PhabricatorPackagesPublisherListController' => 'applications/packages/controller/PhabricatorPackagesPublisherListController.php',
'PhabricatorPackagesPublisherListView' => 'applications/packages/view/PhabricatorPackagesPublisherListView.php',
'PhabricatorPackagesPublisherNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPublisherNameNgrams.php',
'PhabricatorPackagesPublisherNameTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherNameTransaction.php',
'PhabricatorPackagesPublisherPHIDType' => 'applications/packages/phid/PhabricatorPackagesPublisherPHIDType.php',
'PhabricatorPackagesPublisherQuery' => 'applications/packages/query/PhabricatorPackagesPublisherQuery.php',
'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherSearchConduitAPIMethod.php',
'PhabricatorPackagesPublisherSearchEngine' => 'applications/packages/query/PhabricatorPackagesPublisherSearchEngine.php',
'PhabricatorPackagesPublisherTransaction' => 'applications/packages/storage/PhabricatorPackagesPublisherTransaction.php',
'PhabricatorPackagesPublisherTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPublisherTransactionQuery.php',
'PhabricatorPackagesPublisherTransactionType' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherTransactionType.php',
'PhabricatorPackagesPublisherViewController' => 'applications/packages/controller/PhabricatorPackagesPublisherViewController.php',
'PhabricatorPackagesQuery' => 'applications/packages/query/PhabricatorPackagesQuery.php',
'PhabricatorPackagesSchemaSpec' => 'applications/packages/storage/PhabricatorPackagesSchemaSpec.php',
'PhabricatorPackagesTransactionType' => 'applications/packages/xaction/PhabricatorPackagesTransactionType.php',
'PhabricatorPackagesVersion' => 'applications/packages/storage/PhabricatorPackagesVersion.php',
'PhabricatorPackagesVersionController' => 'applications/packages/controller/PhabricatorPackagesVersionController.php',
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionEditConduitAPIMethod.php',
'PhabricatorPackagesVersionEditController' => 'applications/packages/controller/PhabricatorPackagesVersionEditController.php',
'PhabricatorPackagesVersionEditEngine' => 'applications/packages/editor/PhabricatorPackagesVersionEditEngine.php',
'PhabricatorPackagesVersionEditor' => 'applications/packages/editor/PhabricatorPackagesVersionEditor.php',
'PhabricatorPackagesVersionListController' => 'applications/packages/controller/PhabricatorPackagesVersionListController.php',
'PhabricatorPackagesVersionListView' => 'applications/packages/view/PhabricatorPackagesVersionListView.php',
'PhabricatorPackagesVersionNameNgrams' => 'applications/packages/storage/PhabricatorPackagesVersionNameNgrams.php',
'PhabricatorPackagesVersionNameTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionNameTransaction.php',
'PhabricatorPackagesVersionPHIDType' => 'applications/packages/phid/PhabricatorPackagesVersionPHIDType.php',
'PhabricatorPackagesVersionPackageTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionPackageTransaction.php',
'PhabricatorPackagesVersionQuery' => 'applications/packages/query/PhabricatorPackagesVersionQuery.php',
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionSearchConduitAPIMethod.php',
'PhabricatorPackagesVersionSearchEngine' => 'applications/packages/query/PhabricatorPackagesVersionSearchEngine.php',
'PhabricatorPackagesVersionTransaction' => 'applications/packages/storage/PhabricatorPackagesVersionTransaction.php',
'PhabricatorPackagesVersionTransactionQuery' => 'applications/packages/query/PhabricatorPackagesVersionTransactionQuery.php',
'PhabricatorPackagesVersionTransactionType' => 'applications/packages/xaction/version/PhabricatorPackagesVersionTransactionType.php',
'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php',
'PhabricatorPackagesView' => 'applications/packages/view/PhabricatorPackagesView.php',
'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php',
'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php',
'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php',
'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php',
'PhabricatorPasswordSettingsPanel' => 'applications/settings/panel/PhabricatorPasswordSettingsPanel.php',
'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php',
'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php',
'PhabricatorPasteArchiveController' => 'applications/paste/controller/PhabricatorPasteArchiveController.php',
'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php',
'PhabricatorPasteContentSearchEngineAttachment' => 'applications/paste/engineextension/PhabricatorPasteContentSearchEngineAttachment.php',
'PhabricatorPasteContentTransaction' => 'applications/paste/xaction/PhabricatorPasteContentTransaction.php',
'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php',
'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php',
'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php',
'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php',
'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php',
'PhabricatorPasteFilenameContextFreeGrammar' => 'applications/paste/lipsum/PhabricatorPasteFilenameContextFreeGrammar.php',
'PhabricatorPasteLanguageTransaction' => 'applications/paste/xaction/PhabricatorPasteLanguageTransaction.php',
'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php',
'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php',
'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php',
'PhabricatorPasteRawController' => 'applications/paste/controller/PhabricatorPasteRawController.php',
'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php',
'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php',
'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php',
'PhabricatorPasteSnippet' => 'applications/paste/snippet/PhabricatorPasteSnippet.php',
'PhabricatorPasteStatusTransaction' => 'applications/paste/xaction/PhabricatorPasteStatusTransaction.php',
'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php',
'PhabricatorPasteTitleTransaction' => 'applications/paste/xaction/PhabricatorPasteTitleTransaction.php',
'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php',
'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php',
'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php',
'PhabricatorPasteTransactionType' => 'applications/paste/xaction/PhabricatorPasteTransactionType.php',
'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php',
'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php',
'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php',
'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php',
'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
'PhabricatorPeopleBadgesProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php',
'PhabricatorPeopleCommitsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php',
'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php',
'PhabricatorPeopleCreateGuidanceContext' => 'applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php',
'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php',
'PhabricatorPeopleDatasourceEngineExtension' => 'applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php',
'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php',
'PhabricatorPeopleDetailsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php',
'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php',
'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php',
'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php',
'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php',
'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php',
'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php',
'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php',
'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php',
'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php',
'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php',
'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php',
'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php',
'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php',
'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php',
'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php',
'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php',
'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php',
'PhabricatorPeoplePictureProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php',
'PhabricatorPeopleProfileBadgesController' => 'applications/people/controller/PhabricatorPeopleProfileBadgesController.php',
'PhabricatorPeopleProfileCommitsController' => 'applications/people/controller/PhabricatorPeopleProfileCommitsController.php',
'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php',
'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php',
'PhabricatorPeopleProfileImageWorkflow' => 'applications/people/management/PhabricatorPeopleProfileImageWorkflow.php',
'PhabricatorPeopleProfileManageController' => 'applications/people/controller/PhabricatorPeopleProfileManageController.php',
'PhabricatorPeopleProfileMenuEngine' => 'applications/people/engine/PhabricatorPeopleProfileMenuEngine.php',
'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php',
'PhabricatorPeopleProfileRevisionsController' => 'applications/people/controller/PhabricatorPeopleProfileRevisionsController.php',
'PhabricatorPeopleProfileTasksController' => 'applications/people/controller/PhabricatorPeopleProfileTasksController.php',
'PhabricatorPeopleProfileViewController' => 'applications/people/controller/PhabricatorPeopleProfileViewController.php',
'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php',
'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php',
'PhabricatorPeopleRevisionsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php',
'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php',
'PhabricatorPeopleTasksProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php',
'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php',
'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php',
'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php',
'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php',
'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php',
'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php',
'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php',
'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php',
'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php',
'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php',
'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php',
'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
'PhabricatorPhortuneTestCase' => 'applications/phortune/__tests__/PhabricatorPhortuneTestCase.php',
'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php',
'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php',
'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php',
'PhabricatorPhurlConfigOptions' => 'applications/config/option/PhabricatorPhurlConfigOptions.php',
'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php',
'PhabricatorPhurlDAO' => 'applications/phurl/storage/PhabricatorPhurlDAO.php',
'PhabricatorPhurlLinkRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php',
'PhabricatorPhurlRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlRemarkupRule.php',
'PhabricatorPhurlSchemaSpec' => 'applications/phurl/storage/PhabricatorPhurlSchemaSpec.php',
'PhabricatorPhurlShortURLController' => 'applications/phurl/controller/PhabricatorPhurlShortURLController.php',
'PhabricatorPhurlShortURLDefaultController' => 'applications/phurl/controller/PhabricatorPhurlShortURLDefaultController.php',
'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php',
'PhabricatorPhurlURLAccessController' => 'applications/phurl/controller/PhabricatorPhurlURLAccessController.php',
'PhabricatorPhurlURLAliasTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLAliasTransaction.php',
'PhabricatorPhurlURLCreateCapability' => 'applications/phurl/capability/PhabricatorPhurlURLCreateCapability.php',
'PhabricatorPhurlURLDatasource' => 'applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php',
'PhabricatorPhurlURLDescriptionTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLDescriptionTransaction.php',
'PhabricatorPhurlURLEditConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLEditConduitAPIMethod.php',
'PhabricatorPhurlURLEditController' => 'applications/phurl/controller/PhabricatorPhurlURLEditController.php',
'PhabricatorPhurlURLEditEngine' => 'applications/phurl/editor/PhabricatorPhurlURLEditEngine.php',
'PhabricatorPhurlURLEditor' => 'applications/phurl/editor/PhabricatorPhurlURLEditor.php',
'PhabricatorPhurlURLListController' => 'applications/phurl/controller/PhabricatorPhurlURLListController.php',
'PhabricatorPhurlURLLongURLTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLLongURLTransaction.php',
'PhabricatorPhurlURLMailReceiver' => 'applications/phurl/mail/PhabricatorPhurlURLMailReceiver.php',
'PhabricatorPhurlURLNameNgrams' => 'applications/phurl/storage/PhabricatorPhurlURLNameNgrams.php',
'PhabricatorPhurlURLNameTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLNameTransaction.php',
'PhabricatorPhurlURLPHIDType' => 'applications/phurl/phid/PhabricatorPhurlURLPHIDType.php',
'PhabricatorPhurlURLQuery' => 'applications/phurl/query/PhabricatorPhurlURLQuery.php',
'PhabricatorPhurlURLReplyHandler' => 'applications/phurl/mail/PhabricatorPhurlURLReplyHandler.php',
'PhabricatorPhurlURLSearchConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLSearchConduitAPIMethod.php',
'PhabricatorPhurlURLSearchEngine' => 'applications/phurl/query/PhabricatorPhurlURLSearchEngine.php',
'PhabricatorPhurlURLTransaction' => 'applications/phurl/storage/PhabricatorPhurlURLTransaction.php',
'PhabricatorPhurlURLTransactionComment' => 'applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php',
'PhabricatorPhurlURLTransactionQuery' => 'applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php',
'PhabricatorPhurlURLTransactionType' => 'applications/phurl/xaction/PhabricatorPhurlURLTransactionType.php',
'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php',
'PhabricatorPinnedApplicationsSetting' => 'applications/settings/setting/PhabricatorPinnedApplicationsSetting.php',
'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php',
'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php',
'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php',
'PhabricatorPointsFact' => 'applications/fact/fact/PhabricatorPointsFact.php',
'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php',
'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php',
'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php',
'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php',
'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php',
'PhabricatorPolicyCanEditCapability' => 'applications/policy/capability/PhabricatorPolicyCanEditCapability.php',
'PhabricatorPolicyCanInteractCapability' => 'applications/policy/capability/PhabricatorPolicyCanInteractCapability.php',
'PhabricatorPolicyCanJoinCapability' => 'applications/policy/capability/PhabricatorPolicyCanJoinCapability.php',
'PhabricatorPolicyCanViewCapability' => 'applications/policy/capability/PhabricatorPolicyCanViewCapability.php',
'PhabricatorPolicyCapability' => 'applications/policy/capability/PhabricatorPolicyCapability.php',
'PhabricatorPolicyCapabilityTestCase' => 'applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php',
'PhabricatorPolicyCodex' => 'applications/policy/codex/PhabricatorPolicyCodex.php',
'PhabricatorPolicyCodexInterface' => 'applications/policy/codex/PhabricatorPolicyCodexInterface.php',
'PhabricatorPolicyCodexRuleDescription' => 'applications/policy/codex/PhabricatorPolicyCodexRuleDescription.php',
'PhabricatorPolicyConfigOptions' => 'applications/policy/config/PhabricatorPolicyConfigOptions.php',
'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php',
'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php',
'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php',
'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php',
'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php',
'PhabricatorPolicyEditEngineExtension' => 'applications/policy/editor/PhabricatorPolicyEditEngineExtension.php',
'PhabricatorPolicyEditField' => 'applications/transactions/editfield/PhabricatorPolicyEditField.php',
'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php',
'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
'PhabricatorPolicyFavoritesSetting' => 'applications/settings/setting/PhabricatorPolicyFavoritesSetting.php',
'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php',
'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php',
'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php',
'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php',
'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php',
'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php',
'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php',
+ 'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php',
'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php',
'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php',
'PhabricatorProfileMenuEngine' => 'applications/search/engine/PhabricatorProfileMenuEngine.php',
'PhabricatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorProfileMenuItem.php',
'PhabricatorProfileMenuItemConfiguration' => 'applications/search/storage/PhabricatorProfileMenuItemConfiguration.php',
'PhabricatorProfileMenuItemConfigurationQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationQuery.php',
'PhabricatorProfileMenuItemConfigurationTransaction' => 'applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php',
'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationTransactionQuery.php',
'PhabricatorProfileMenuItemIconSet' => 'applications/search/menuitem/PhabricatorProfileMenuItemIconSet.php',
'PhabricatorProfileMenuItemPHIDType' => 'applications/search/phidtype/PhabricatorProfileMenuItemPHIDType.php',
'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php',
'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php',
'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php',
'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php',
'PhabricatorProjectBoardBackgroundController' => 'applications/project/controller/PhabricatorProjectBoardBackgroundController.php',
'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php',
'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php',
'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php',
'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php',
'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php',
'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php',
'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php',
'PhabricatorProjectCardView' => 'applications/project/view/PhabricatorProjectCardView.php',
'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php',
'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php',
'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php',
'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php',
'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php',
'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php',
'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php',
'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php',
'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php',
'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php',
'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php',
'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php',
'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php',
'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
'PhabricatorProjectCoreTestCase' => 'applications/project/__tests__/PhabricatorProjectCoreTestCase.php',
'PhabricatorProjectCoverController' => 'applications/project/controller/PhabricatorProjectCoverController.php',
'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php',
'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php',
'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php',
'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php',
'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php',
'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php',
'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php',
'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php',
'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php',
'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php',
'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
'PhabricatorProjectFerretEngine' => 'applications/project/search/PhabricatorProjectFerretEngine.php',
'PhabricatorProjectFilterTransaction' => 'applications/project/xaction/PhabricatorProjectFilterTransaction.php',
'PhabricatorProjectFulltextEngine' => 'applications/project/search/PhabricatorProjectFulltextEngine.php',
'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php',
'PhabricatorProjectHeraldAdapter' => 'applications/project/herald/PhabricatorProjectHeraldAdapter.php',
'PhabricatorProjectHeraldFieldGroup' => 'applications/project/herald/PhabricatorProjectHeraldFieldGroup.php',
'PhabricatorProjectHovercardEngineExtension' => 'applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php',
'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php',
'PhabricatorProjectIconTransaction' => 'applications/project/xaction/PhabricatorProjectIconTransaction.php',
'PhabricatorProjectIconsConfigType' => 'applications/project/config/PhabricatorProjectIconsConfigType.php',
'PhabricatorProjectImageTransaction' => 'applications/project/xaction/PhabricatorProjectImageTransaction.php',
'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php',
'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php',
'PhabricatorProjectLockTransaction' => 'applications/project/xaction/PhabricatorProjectLockTransaction.php',
'PhabricatorProjectLogicalAncestorDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php',
'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
'PhabricatorProjectLogicalOnlyDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php',
'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php',
'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php',
'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php',
'PhabricatorProjectManageController' => 'applications/project/controller/PhabricatorProjectManageController.php',
'PhabricatorProjectManageProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php',
'PhabricatorProjectMaterializedMemberEdgeType' => 'applications/project/edge/PhabricatorProjectMaterializedMemberEdgeType.php',
'PhabricatorProjectMemberListView' => 'applications/project/view/PhabricatorProjectMemberListView.php',
'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php',
'PhabricatorProjectMembersAddController' => 'applications/project/controller/PhabricatorProjectMembersAddController.php',
'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php',
'PhabricatorProjectMembersPolicyRule' => 'applications/project/policyrule/PhabricatorProjectMembersPolicyRule.php',
'PhabricatorProjectMembersProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php',
'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php',
'PhabricatorProjectMenuItemController' => 'applications/project/controller/PhabricatorProjectMenuItemController.php',
'PhabricatorProjectMilestoneTransaction' => 'applications/project/xaction/PhabricatorProjectMilestoneTransaction.php',
'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php',
'PhabricatorProjectNameTransaction' => 'applications/project/xaction/PhabricatorProjectNameTransaction.php',
'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php',
'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php',
'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php',
'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php',
'PhabricatorProjectParentTransaction' => 'applications/project/xaction/PhabricatorProjectParentTransaction.php',
'PhabricatorProjectPictureProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php',
'PhabricatorProjectPointsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php',
'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php',
'PhabricatorProjectProfileMenuEngine' => 'applications/project/engine/PhabricatorProjectProfileMenuEngine.php',
'PhabricatorProjectProfileMenuItem' => 'applications/search/menuitem/PhabricatorProjectProfileMenuItem.php',
'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php',
'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php',
'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php',
'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php',
'PhabricatorProjectRemoveHeraldAction' => 'applications/project/herald/PhabricatorProjectRemoveHeraldAction.php',
'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php',
'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php',
'PhabricatorProjectSearchField' => 'applications/project/searchfield/PhabricatorProjectSearchField.php',
'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php',
'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php',
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
'PhabricatorProjectSlugsTransaction' => 'applications/project/xaction/PhabricatorProjectSlugsTransaction.php',
'PhabricatorProjectSortTransaction' => 'applications/project/xaction/PhabricatorProjectSortTransaction.php',
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
'PhabricatorProjectStatusTransaction' => 'applications/project/xaction/PhabricatorProjectStatusTransaction.php',
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php',
'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php',
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php',
'PhabricatorProjectUserListView' => 'applications/project/view/PhabricatorProjectUserListView.php',
'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php',
'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php',
'PhabricatorProjectWorkboardBackgroundColor' => 'applications/project/constants/PhabricatorProjectWorkboardBackgroundColor.php',
'PhabricatorProjectWorkboardBackgroundTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardBackgroundTransaction.php',
'PhabricatorProjectWorkboardProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php',
'PhabricatorProjectWorkboardTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardTransaction.php',
+ 'PhabricatorProjectsAllPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsAncestorsSearchEngineAttachment.php',
+ 'PhabricatorProjectsBasePolicyRule' => 'applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php',
'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php',
'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php',
'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php',
'PhabricatorProjectsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php',
'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php',
'PhabricatorProjectsMailEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsMembersSearchEngineAttachment.php',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php',
'PhabricatorProjectsPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsPolicyRule.php',
'PhabricatorProjectsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineAttachment.php',
'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php',
'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php',
'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
+ 'PhabricatorQueryIterator' => 'infrastructure/storage/lisk/PhabricatorQueryIterator.php',
'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
'PhabricatorQuickSearchEngineExtension' => 'applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php',
'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php',
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
'PhabricatorRegexListConfigType' => 'applications/config/type/PhabricatorRegexListConfigType.php',
'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
'PhabricatorRemarkupCachePurger' => 'applications/cache/purger/PhabricatorRemarkupCachePurger.php',
'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php',
'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php',
'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php',
'PhabricatorRemarkupDocumentEngine' => 'applications/files/document/PhabricatorRemarkupDocumentEngine.php',
'PhabricatorRemarkupEditField' => 'applications/transactions/editfield/PhabricatorRemarkupEditField.php',
'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php',
'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php',
'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php',
'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php',
'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php',
'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php',
'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php',
'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php',
'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php',
'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php',
'PhabricatorRepositoryCommitHint' => 'applications/repository/storage/PhabricatorRepositoryCommitHint.php',
'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php',
'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php',
'PhabricatorRepositoryCommitPHIDType' => 'applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php',
'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php',
'PhabricatorRepositoryCommitRef' => 'applications/repository/engine/PhabricatorRepositoryCommitRef.php',
'PhabricatorRepositoryCommitTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryCommitTestCase.php',
'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php',
'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php',
'PhabricatorRepositoryDestructibleCodex' => 'applications/repository/codex/PhabricatorRepositoryDestructibleCodex.php',
'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
'PhabricatorRepositoryFerretEngine' => 'applications/repository/search/PhabricatorRepositoryFerretEngine.php',
'PhabricatorRepositoryFulltextEngine' => 'applications/repository/search/PhabricatorRepositoryFulltextEngine.php',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php',
'PhabricatorRepositoryGitLFSRef' => 'applications/repository/storage/PhabricatorRepositoryGitLFSRef.php',
'PhabricatorRepositoryGitLFSRefQuery' => 'applications/repository/query/PhabricatorRepositoryGitLFSRefQuery.php',
'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php',
'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php',
+ 'PhabricatorRepositoryIdentity' => 'applications/repository/storage/PhabricatorRepositoryIdentity.php',
+ 'PhabricatorRepositoryIdentityAssignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php',
+ 'PhabricatorRepositoryIdentityChangeWorker' => 'applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php',
+ 'PhabricatorRepositoryIdentityEditEngine' => 'applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php',
+ 'PhabricatorRepositoryIdentityFerretEngine' => 'applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php',
+ 'PhabricatorRepositoryIdentityPHIDType' => 'applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php',
+ 'PhabricatorRepositoryIdentityQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityQuery.php',
+ 'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php',
+ 'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php',
+ 'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php',
'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php',
'PhabricatorRepositoryManagementHintWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementHintWorkflow.php',
'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php',
'PhabricatorRepositoryManagementListPathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListPathsWorkflow.php',
'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php',
'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php',
'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php',
'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php',
'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php',
'PhabricatorRepositoryManagementMovePathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMovePathsWorkflow.php',
'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php',
'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php',
+ 'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php',
'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php',
'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php',
'PhabricatorRepositoryManagementThawWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php',
'PhabricatorRepositoryManagementUnpublishWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php',
'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php',
'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php',
'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php',
'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php',
'PhabricatorRepositoryOldRef' => 'applications/repository/storage/PhabricatorRepositoryOldRef.php',
'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php',
'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php',
'PhabricatorRepositoryPullEvent' => 'applications/repository/storage/PhabricatorRepositoryPullEvent.php',
'PhabricatorRepositoryPullEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPullEventPHIDType.php',
'PhabricatorRepositoryPullEventQuery' => 'applications/repository/query/PhabricatorRepositoryPullEventQuery.php',
'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php',
'PhabricatorRepositoryPullLocalDaemonModule' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php',
'PhabricatorRepositoryPushEvent' => 'applications/repository/storage/PhabricatorRepositoryPushEvent.php',
'PhabricatorRepositoryPushEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushEventPHIDType.php',
'PhabricatorRepositoryPushEventQuery' => 'applications/repository/query/PhabricatorRepositoryPushEventQuery.php',
'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php',
'PhabricatorRepositoryPushLogPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushLogPHIDType.php',
'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php',
'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php',
'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php',
'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php',
'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php',
'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php',
'PhabricatorRepositoryRefCursorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRefCursorPHIDType.php',
'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php',
'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
'PhabricatorRepositoryRefPosition' => 'applications/repository/storage/PhabricatorRepositoryRefPosition.php',
'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php',
'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php',
'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php',
'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php',
'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php',
'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php',
'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
'PhabricatorRepositoryURIPHIDType' => 'applications/repository/phid/PhabricatorRepositoryURIPHIDType.php',
'PhabricatorRepositoryURIQuery' => 'applications/repository/query/PhabricatorRepositoryURIQuery.php',
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php',
'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php',
'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php',
'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php',
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php',
'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php',
'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php',
'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php',
'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php',
'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php',
'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php',
'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php',
'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php',
'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php',
'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php',
'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php',
'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php',
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php',
'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php',
'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php',
'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php',
'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php',
'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php',
'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php',
'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php',
'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php',
'PhabricatorSearchDateControlField' => 'applications/search/field/PhabricatorSearchDateControlField.php',
'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php',
'PhabricatorSearchDefaultController' => 'applications/search/controller/PhabricatorSearchDefaultController.php',
'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php',
'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php',
'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php',
'PhabricatorSearchDocumentFieldType' => 'applications/search/constants/PhabricatorSearchDocumentFieldType.php',
'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php',
'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php',
'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php',
'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php',
'PhabricatorSearchEngineAPIMethod' => 'applications/search/engine/PhabricatorSearchEngineAPIMethod.php',
'PhabricatorSearchEngineAttachment' => 'applications/search/engineextension/PhabricatorSearchEngineAttachment.php',
'PhabricatorSearchEngineExtension' => 'applications/search/engineextension/PhabricatorSearchEngineExtension.php',
'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php',
'PhabricatorSearchFerretNgramGarbageCollector' => 'applications/search/garbagecollector/PhabricatorSearchFerretNgramGarbageCollector.php',
'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php',
'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php',
'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php',
'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php',
'PhabricatorSearchManagementNgramsWorkflow' => 'applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php',
'PhabricatorSearchManagementQueryWorkflow' => 'applications/search/management/PhabricatorSearchManagementQueryWorkflow.php',
'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php',
'PhabricatorSearchNgrams' => 'applications/search/ngrams/PhabricatorSearchNgrams.php',
'PhabricatorSearchNgramsDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchNgramsDestructionEngineExtension.php',
'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php',
'PhabricatorSearchRelationshipSourceController' => 'applications/search/controller/PhabricatorSearchRelationshipSourceController.php',
'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php',
'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php',
'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
'PhabricatorSearchThreeStateField' => 'applications/search/field/PhabricatorSearchThreeStateField.php',
'PhabricatorSearchTokenizerField' => 'applications/search/field/PhabricatorSearchTokenizerField.php',
'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php',
'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php',
'PhabricatorSelectSetting' => 'applications/settings/setting/PhabricatorSelectSetting.php',
'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php',
'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php',
'PhabricatorSetConfigType' => 'applications/config/type/PhabricatorSetConfigType.php',
'PhabricatorSetting' => 'applications/settings/setting/PhabricatorSetting.php',
'PhabricatorSettingsAccountPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAccountPanelGroup.php',
'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php',
'PhabricatorSettingsApplicationsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsApplicationsPanelGroup.php',
'PhabricatorSettingsAuthenticationPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAuthenticationPanelGroup.php',
'PhabricatorSettingsDeveloperPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsDeveloperPanelGroup.php',
'PhabricatorSettingsEditEngine' => 'applications/settings/editor/PhabricatorSettingsEditEngine.php',
'PhabricatorSettingsEmailPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsEmailPanelGroup.php',
'PhabricatorSettingsIssueController' => 'applications/settings/controller/PhabricatorSettingsIssueController.php',
'PhabricatorSettingsListController' => 'applications/settings/controller/PhabricatorSettingsListController.php',
'PhabricatorSettingsLogsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsLogsPanelGroup.php',
'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
'PhabricatorSettingsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsPanelGroup.php',
'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php',
'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php',
'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php',
'PhabricatorSetupEngine' => 'applications/config/engine/PhabricatorSetupEngine.php',
'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php',
'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php',
'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php',
'PhabricatorSite' => 'aphront/site/PhabricatorSite.php',
'PhabricatorSlackAuthProvider' => 'applications/auth/provider/PhabricatorSlackAuthProvider.php',
'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php',
'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php',
'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php',
'PhabricatorSlowvoteCloseTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteCloseTransaction.php',
'PhabricatorSlowvoteCommentController' => 'applications/slowvote/controller/PhabricatorSlowvoteCommentController.php',
'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php',
'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php',
'PhabricatorSlowvoteDefaultViewCapability' => 'applications/slowvote/capability/PhabricatorSlowvoteDefaultViewCapability.php',
'PhabricatorSlowvoteDescriptionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteDescriptionTransaction.php',
'PhabricatorSlowvoteEditController' => 'applications/slowvote/controller/PhabricatorSlowvoteEditController.php',
'PhabricatorSlowvoteEditor' => 'applications/slowvote/editor/PhabricatorSlowvoteEditor.php',
'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php',
'PhabricatorSlowvoteMailReceiver' => 'applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php',
'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php',
'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php',
'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php',
'PhabricatorSlowvotePollPHIDType' => 'applications/slowvote/phid/PhabricatorSlowvotePollPHIDType.php',
'PhabricatorSlowvoteQuery' => 'applications/slowvote/query/PhabricatorSlowvoteQuery.php',
'PhabricatorSlowvoteQuestionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteQuestionTransaction.php',
'PhabricatorSlowvoteReplyHandler' => 'applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php',
'PhabricatorSlowvoteResponsesTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteResponsesTransaction.php',
'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php',
'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php',
'PhabricatorSlowvoteShuffleTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteShuffleTransaction.php',
'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php',
'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php',
'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php',
'PhabricatorSlowvoteTransactionType' => 'applications/slowvote/xaction/PhabricatorSlowvoteTransactionType.php',
'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php',
'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php',
'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php',
'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
'PhabricatorSourceDocumentEngine' => 'applications/files/document/PhabricatorSourceDocumentEngine.php',
'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php',
'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php',
'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php',
'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php',
'PhabricatorSpacesCapabilityDefaultEdit' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultEdit.php',
'PhabricatorSpacesCapabilityDefaultView' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultView.php',
'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php',
'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php',
'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php',
'PhabricatorSpacesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php',
'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php',
'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php',
'PhabricatorSpacesMailEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php',
'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php',
'PhabricatorSpacesNamespaceArchiveTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceArchiveTransaction.php',
'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php',
'PhabricatorSpacesNamespaceDefaultTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDefaultTransaction.php',
'PhabricatorSpacesNamespaceDescriptionTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDescriptionTransaction.php',
'PhabricatorSpacesNamespaceEditor' => 'applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php',
'PhabricatorSpacesNamespaceNameTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php',
'PhabricatorSpacesNamespacePHIDType' => 'applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php',
'PhabricatorSpacesNamespaceQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceQuery.php',
'PhabricatorSpacesNamespaceSearchEngine' => 'applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php',
'PhabricatorSpacesNamespaceTransaction' => 'applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php',
'PhabricatorSpacesNamespaceTransactionQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceTransactionQuery.php',
'PhabricatorSpacesNamespaceTransactionType' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceTransactionType.php',
'PhabricatorSpacesNoAccessController' => 'applications/spaces/controller/PhabricatorSpacesNoAccessController.php',
'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php',
'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.php',
'PhabricatorSpacesSearchEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php',
'PhabricatorSpacesSearchField' => 'applications/spaces/searchfield/PhabricatorSpacesSearchField.php',
'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php',
'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php',
'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php',
'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php',
'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php',
'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php',
'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php',
'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php',
'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php',
'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php',
'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
'PhabricatorStandardCustomFieldLink' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php',
'PhabricatorStandardCustomFieldPHIDs' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php',
'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php',
'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php',
'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php',
'PhabricatorStandardCustomFieldTokenizer' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php',
'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php',
'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php',
'PhabricatorStaticEditField' => 'applications/transactions/editfield/PhabricatorStaticEditField.php',
'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php',
'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php',
'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php',
'PhabricatorStorageManagementAdjustWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php',
'PhabricatorStorageManagementAnalyzeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAnalyzeWorkflow.php',
'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php',
'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php',
'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php',
'PhabricatorStorageManagementOptimizeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementOptimizeWorkflow.php',
'PhabricatorStorageManagementPartitionWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementPartitionWorkflow.php',
'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php',
'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php',
'PhabricatorStorageManagementShellWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php',
'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php',
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php',
'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php',
'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php',
'PhabricatorStringExportField' => 'infrastructure/export/field/PhabricatorStringExportField.php',
'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php',
'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php',
'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php',
'PhabricatorStringMailStamp' => 'applications/metamta/stamp/PhabricatorStringMailStamp.php',
'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php',
'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php',
'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.php',
'PhabricatorSubscribersEditField' => 'applications/transactions/editfield/PhabricatorSubscribersEditField.php',
'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
'PhabricatorSubscriptionTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php',
'PhabricatorSubscriptionsAddSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php',
'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php',
'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php',
'PhabricatorSubscriptionsCurtainExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php',
'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php',
'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
'PhabricatorSubscriptionsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php',
'PhabricatorSubscriptionsFulltextEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsFulltextEngineExtension.php',
'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php',
'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php',
'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php',
'PhabricatorSubscriptionsSearchEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php',
'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php',
'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php',
'PhabricatorSubtypeEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php',
'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php',
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php',
'PhabricatorSyntaxStyle' => 'infrastructure/syntax/PhabricatorSyntaxStyle.php',
'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php',
'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php',
'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php',
'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php',
'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php',
'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php',
'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
+ 'PhabricatorSystemObjectController' => 'applications/system/controller/PhabricatorSystemObjectController.php',
'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php',
'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php',
'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php',
'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php',
'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php',
'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php',
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
'PhabricatorTaskmasterDaemonModule' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php',
'PhabricatorTestApplication' => 'applications/base/controller/__tests__/PhabricatorTestApplication.php',
'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php',
'PhabricatorTestDataGenerator' => 'applications/lipsum/generator/PhabricatorTestDataGenerator.php',
'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php',
'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php',
'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php',
'PhabricatorTextDocumentEngine' => 'applications/files/document/PhabricatorTextDocumentEngine.php',
'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php',
'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php',
'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php',
'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php',
'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'applications/settings/setting/PhabricatorTimezoneIgnoreOffsetSetting.php',
'PhabricatorTimezoneSetting' => 'applications/settings/setting/PhabricatorTimezoneSetting.php',
'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php',
'PhabricatorTitleGlyphsSetting' => 'applications/settings/setting/PhabricatorTitleGlyphsSetting.php',
'PhabricatorToken' => 'applications/tokens/storage/PhabricatorToken.php',
'PhabricatorTokenController' => 'applications/tokens/controller/PhabricatorTokenController.php',
'PhabricatorTokenCount' => 'applications/tokens/storage/PhabricatorTokenCount.php',
'PhabricatorTokenCountQuery' => 'applications/tokens/query/PhabricatorTokenCountQuery.php',
'PhabricatorTokenDAO' => 'applications/tokens/storage/PhabricatorTokenDAO.php',
'PhabricatorTokenDestructionEngineExtension' => 'applications/tokens/engineextension/PhabricatorTokenDestructionEngineExtension.php',
'PhabricatorTokenGiveController' => 'applications/tokens/controller/PhabricatorTokenGiveController.php',
'PhabricatorTokenGiven' => 'applications/tokens/storage/PhabricatorTokenGiven.php',
'PhabricatorTokenGivenController' => 'applications/tokens/controller/PhabricatorTokenGivenController.php',
'PhabricatorTokenGivenEditor' => 'applications/tokens/editor/PhabricatorTokenGivenEditor.php',
'PhabricatorTokenGivenFeedStory' => 'applications/tokens/feed/PhabricatorTokenGivenFeedStory.php',
'PhabricatorTokenGivenQuery' => 'applications/tokens/query/PhabricatorTokenGivenQuery.php',
'PhabricatorTokenLeaderController' => 'applications/tokens/controller/PhabricatorTokenLeaderController.php',
'PhabricatorTokenQuery' => 'applications/tokens/query/PhabricatorTokenQuery.php',
'PhabricatorTokenReceiverInterface' => 'applications/tokens/interface/PhabricatorTokenReceiverInterface.php',
'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php',
'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php',
'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php',
'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php',
'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php',
'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php',
'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php',
'PhabricatorTokensToken' => 'applications/tokens/storage/PhabricatorTokensToken.php',
'PhabricatorTransactionChange' => 'applications/transactions/data/PhabricatorTransactionChange.php',
'PhabricatorTransactionFactEngine' => 'applications/fact/engine/PhabricatorTransactionFactEngine.php',
'PhabricatorTransactionRemarkupChange' => 'applications/transactions/data/PhabricatorTransactionRemarkupChange.php',
'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php',
'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php',
'PhabricatorTransactionsDestructionEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsDestructionEngineExtension.php',
'PhabricatorTransactionsFulltextEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php',
'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php',
'PhabricatorTranslationSetting' => 'applications/settings/setting/PhabricatorTranslationSetting.php',
'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php',
'PhabricatorTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorTriggerAction.php',
'PhabricatorTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorTriggerClock.php',
'PhabricatorTriggerClockTestCase' => 'infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php',
'PhabricatorTriggerDaemon' => 'infrastructure/daemon/workers/PhabricatorTriggerDaemon.php',
'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php',
'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php',
'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php',
'PhabricatorTypeaheadApplication' => 'applications/typeahead/application/PhabricatorTypeaheadApplication.php',
'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php',
'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php',
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php',
'PhabricatorTypeaheadDatasourceTestCase' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php',
'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php',
'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php',
'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php',
'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php',
'PhabricatorTypeaheadProxyDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php',
'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php',
'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php',
'PhabricatorTypeaheadTestNumbersDatasource' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php',
'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php',
'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php',
'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php',
'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php',
'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php',
'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php',
'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php',
'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php',
'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php',
'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php',
'PhabricatorUserCachePurger' => 'applications/cache/purger/PhabricatorUserCachePurger.php',
'PhabricatorUserCacheType' => 'applications/people/cache/PhabricatorUserCacheType.php',
'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php',
'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php',
'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php',
'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
+ 'PhabricatorUserDisableTransaction' => 'applications/people/xaction/PhabricatorUserDisableTransaction.php',
+ 'PhabricatorUserEditEngine' => 'applications/people/editor/PhabricatorUserEditEngine.php',
'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php',
'PhabricatorUserFerretEngine' => 'applications/people/search/PhabricatorUserFerretEngine.php',
'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php',
'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php',
'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php',
'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php',
'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php',
'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php',
'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php',
'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php',
'PhabricatorUserPreferencesEditor' => 'applications/settings/editor/PhabricatorUserPreferencesEditor.php',
'PhabricatorUserPreferencesPHIDType' => 'applications/settings/phid/PhabricatorUserPreferencesPHIDType.php',
'PhabricatorUserPreferencesQuery' => 'applications/settings/query/PhabricatorUserPreferencesQuery.php',
'PhabricatorUserPreferencesSearchEngine' => 'applications/settings/query/PhabricatorUserPreferencesSearchEngine.php',
'PhabricatorUserPreferencesTransaction' => 'applications/settings/storage/PhabricatorUserPreferencesTransaction.php',
'PhabricatorUserPreferencesTransactionQuery' => 'applications/settings/query/PhabricatorUserPreferencesTransactionQuery.php',
'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
- 'PhabricatorUserProfileEditor' => 'applications/people/editor/PhabricatorUserProfileEditor.php',
'PhabricatorUserProfileImageCacheType' => 'applications/people/cache/PhabricatorUserProfileImageCacheType.php',
'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php',
'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php',
'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php',
'PhabricatorUserSinceField' => 'applications/people/customfield/PhabricatorUserSinceField.php',
'PhabricatorUserStatusField' => 'applications/people/customfield/PhabricatorUserStatusField.php',
'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php',
'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
+ 'PhabricatorUserTransactionEditor' => 'applications/people/editor/PhabricatorUserTransactionEditor.php',
+ 'PhabricatorUserTransactionType' => 'applications/people/xaction/PhabricatorUserTransactionType.php',
'PhabricatorUsersEditField' => 'applications/transactions/editfield/PhabricatorUsersEditField.php',
'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php',
'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php',
'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php',
'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php',
'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php',
'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php',
'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php',
'PhabricatorVoidDocumentEngine' => 'applications/files/document/PhabricatorVoidDocumentEngine.php',
'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php',
'PhabricatorWebContentSource' => 'infrastructure/contentsource/PhabricatorWebContentSource.php',
'PhabricatorWebServerSetupCheck' => 'applications/config/check/PhabricatorWebServerSetupCheck.php',
'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php',
'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php',
'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php',
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php',
'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php',
'PhabricatorWorkerBulkJobCreateWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php',
'PhabricatorWorkerBulkJobEditor' => 'infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php',
'PhabricatorWorkerBulkJobPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php',
'PhabricatorWorkerBulkJobQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php',
'PhabricatorWorkerBulkJobSearchEngine' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php',
'PhabricatorWorkerBulkJobTaskWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php',
'PhabricatorWorkerBulkJobTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php',
'PhabricatorWorkerBulkJobTransaction' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php',
'PhabricatorWorkerBulkJobTransactionQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php',
'PhabricatorWorkerBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php',
'PhabricatorWorkerBulkJobWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php',
'PhabricatorWorkerBulkTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php',
'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
'PhabricatorWorkerDestructionEngineExtension' => 'infrastructure/daemon/workers/engineextension/PhabricatorWorkerDestructionEngineExtension.php',
'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php',
'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php',
'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php',
'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php',
'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php',
'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php',
'PhabricatorWorkerSingleBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php',
'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
'PhabricatorWorkerTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php',
'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php',
'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php',
'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php',
'PhabricatorWorkerTriggerManagementFireWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php',
'PhabricatorWorkerTriggerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php',
'PhabricatorWorkerTriggerPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerTriggerPHIDType.php',
'PhabricatorWorkerTriggerQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php',
'PhabricatorWorkerYieldException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php',
'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php',
'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php',
'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php',
'PhabricatorXHPASTDAO' => 'applications/phpast/storage/PhabricatorXHPASTDAO.php',
'PhabricatorXHPASTParseTree' => 'applications/phpast/storage/PhabricatorXHPASTParseTree.php',
'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php',
'PhabricatorXHPASTViewFrameController' => 'applications/phpast/controller/PhabricatorXHPASTViewFrameController.php',
'PhabricatorXHPASTViewFramesetController' => 'applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php',
'PhabricatorXHPASTViewInputController' => 'applications/phpast/controller/PhabricatorXHPASTViewInputController.php',
'PhabricatorXHPASTViewPanelController' => 'applications/phpast/controller/PhabricatorXHPASTViewPanelController.php',
'PhabricatorXHPASTViewRunController' => 'applications/phpast/controller/PhabricatorXHPASTViewRunController.php',
'PhabricatorXHPASTViewStreamController' => 'applications/phpast/controller/PhabricatorXHPASTViewStreamController.php',
'PhabricatorXHPASTViewTreeController' => 'applications/phpast/controller/PhabricatorXHPASTViewTreeController.php',
'PhabricatorXHProfApplication' => 'applications/xhprof/application/PhabricatorXHProfApplication.php',
'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php',
'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php',
'PhabricatorXHProfDropController' => 'applications/xhprof/controller/PhabricatorXHProfDropController.php',
'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php',
'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php',
'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php',
'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php',
'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php',
'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php',
'PhabricatorXHProfSampleQuery' => 'applications/xhprof/query/PhabricatorXHProfSampleQuery.php',
'PhabricatorXHProfSampleSearchEngine' => 'applications/xhprof/query/PhabricatorXHProfSampleSearchEngine.php',
'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php',
'Phame404Response' => 'applications/phame/site/Phame404Response.php',
'PhameBlog' => 'applications/phame/storage/PhameBlog.php',
'PhameBlog404Controller' => 'applications/phame/controller/blog/PhameBlog404Controller.php',
'PhameBlogArchiveController' => 'applications/phame/controller/blog/PhameBlogArchiveController.php',
'PhameBlogController' => 'applications/phame/controller/blog/PhameBlogController.php',
'PhameBlogCreateCapability' => 'applications/phame/capability/PhameBlogCreateCapability.php',
'PhameBlogDatasource' => 'applications/phame/typeahead/PhameBlogDatasource.php',
'PhameBlogDescriptionTransaction' => 'applications/phame/xaction/PhameBlogDescriptionTransaction.php',
'PhameBlogEditConduitAPIMethod' => 'applications/phame/conduit/PhameBlogEditConduitAPIMethod.php',
'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php',
'PhameBlogEditEngine' => 'applications/phame/editor/PhameBlogEditEngine.php',
'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php',
'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php',
'PhameBlogFerretEngine' => 'applications/phame/search/PhameBlogFerretEngine.php',
'PhameBlogFullDomainTransaction' => 'applications/phame/xaction/PhameBlogFullDomainTransaction.php',
'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php',
'PhameBlogHeaderImageTransaction' => 'applications/phame/xaction/PhameBlogHeaderImageTransaction.php',
'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php',
'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php',
'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php',
'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php',
'PhameBlogNameTransaction' => 'applications/phame/xaction/PhameBlogNameTransaction.php',
'PhameBlogParentDomainTransaction' => 'applications/phame/xaction/PhameBlogParentDomainTransaction.php',
'PhameBlogParentSiteTransaction' => 'applications/phame/xaction/PhameBlogParentSiteTransaction.php',
'PhameBlogProfileImageTransaction' => 'applications/phame/xaction/PhameBlogProfileImageTransaction.php',
'PhameBlogProfilePictureController' => 'applications/phame/controller/blog/PhameBlogProfilePictureController.php',
'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php',
'PhameBlogReplyHandler' => 'applications/phame/mail/PhameBlogReplyHandler.php',
'PhameBlogSearchConduitAPIMethod' => 'applications/phame/conduit/PhameBlogSearchConduitAPIMethod.php',
'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php',
'PhameBlogSite' => 'applications/phame/site/PhameBlogSite.php',
'PhameBlogStatusTransaction' => 'applications/phame/xaction/PhameBlogStatusTransaction.php',
'PhameBlogSubtitleTransaction' => 'applications/phame/xaction/PhameBlogSubtitleTransaction.php',
'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php',
'PhameBlogTransactionQuery' => 'applications/phame/query/PhameBlogTransactionQuery.php',
'PhameBlogTransactionType' => 'applications/phame/xaction/PhameBlogTransactionType.php',
'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php',
'PhameConstants' => 'applications/phame/constants/PhameConstants.php',
'PhameController' => 'applications/phame/controller/PhameController.php',
'PhameDAO' => 'applications/phame/storage/PhameDAO.php',
'PhameDescriptionView' => 'applications/phame/view/PhameDescriptionView.php',
'PhameDraftListView' => 'applications/phame/view/PhameDraftListView.php',
'PhameHomeController' => 'applications/phame/controller/PhameHomeController.php',
'PhameLiveController' => 'applications/phame/controller/PhameLiveController.php',
'PhameNextPostView' => 'applications/phame/view/PhameNextPostView.php',
'PhamePost' => 'applications/phame/storage/PhamePost.php',
'PhamePostArchiveController' => 'applications/phame/controller/post/PhamePostArchiveController.php',
'PhamePostBlogTransaction' => 'applications/phame/xaction/PhamePostBlogTransaction.php',
'PhamePostBodyTransaction' => 'applications/phame/xaction/PhamePostBodyTransaction.php',
'PhamePostController' => 'applications/phame/controller/post/PhamePostController.php',
'PhamePostEditConduitAPIMethod' => 'applications/phame/conduit/PhamePostEditConduitAPIMethod.php',
'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php',
'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php',
'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php',
'PhamePostFerretEngine' => 'applications/phame/search/PhamePostFerretEngine.php',
'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php',
'PhamePostHeaderImageTransaction' => 'applications/phame/xaction/PhamePostHeaderImageTransaction.php',
'PhamePostHeaderPictureController' => 'applications/phame/controller/post/PhamePostHeaderPictureController.php',
'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php',
'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php',
'PhamePostListView' => 'applications/phame/view/PhamePostListView.php',
'PhamePostMailReceiver' => 'applications/phame/mail/PhamePostMailReceiver.php',
'PhamePostMoveController' => 'applications/phame/controller/post/PhamePostMoveController.php',
'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php',
'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php',
'PhamePostRemarkupRule' => 'applications/phame/remarkup/PhamePostRemarkupRule.php',
'PhamePostReplyHandler' => 'applications/phame/mail/PhamePostReplyHandler.php',
'PhamePostSearchConduitAPIMethod' => 'applications/phame/conduit/PhamePostSearchConduitAPIMethod.php',
'PhamePostSearchEngine' => 'applications/phame/query/PhamePostSearchEngine.php',
'PhamePostSubtitleTransaction' => 'applications/phame/xaction/PhamePostSubtitleTransaction.php',
'PhamePostTitleTransaction' => 'applications/phame/xaction/PhamePostTitleTransaction.php',
'PhamePostTransaction' => 'applications/phame/storage/PhamePostTransaction.php',
'PhamePostTransactionComment' => 'applications/phame/storage/PhamePostTransactionComment.php',
'PhamePostTransactionQuery' => 'applications/phame/query/PhamePostTransactionQuery.php',
'PhamePostTransactionType' => 'applications/phame/xaction/PhamePostTransactionType.php',
'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php',
'PhamePostVisibilityTransaction' => 'applications/phame/xaction/PhamePostVisibilityTransaction.php',
'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php',
'PhameSite' => 'applications/phame/site/PhameSite.php',
'PhluxController' => 'applications/phlux/controller/PhluxController.php',
'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php',
'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php',
'PhluxListController' => 'applications/phlux/controller/PhluxListController.php',
+ 'PhluxSchemaSpec' => 'applications/phlux/storage/PhluxSchemaSpec.php',
'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php',
'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php',
'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php',
'PhluxVariableEditor' => 'applications/phlux/editor/PhluxVariableEditor.php',
'PhluxVariablePHIDType' => 'applications/phlux/phid/PhluxVariablePHIDType.php',
'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php',
'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php',
'PholioController' => 'applications/pholio/controller/PholioController.php',
'PholioDAO' => 'applications/pholio/storage/PholioDAO.php',
'PholioDefaultEditCapability' => 'applications/pholio/capability/PholioDefaultEditCapability.php',
'PholioDefaultViewCapability' => 'applications/pholio/capability/PholioDefaultViewCapability.php',
'PholioImage' => 'applications/pholio/storage/PholioImage.php',
'PholioImageDescriptionTransaction' => 'applications/pholio/xaction/PholioImageDescriptionTransaction.php',
'PholioImageFileTransaction' => 'applications/pholio/xaction/PholioImageFileTransaction.php',
'PholioImageNameTransaction' => 'applications/pholio/xaction/PholioImageNameTransaction.php',
'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php',
'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php',
'PholioImageReplaceTransaction' => 'applications/pholio/xaction/PholioImageReplaceTransaction.php',
'PholioImageSequenceTransaction' => 'applications/pholio/xaction/PholioImageSequenceTransaction.php',
'PholioImageTransactionType' => 'applications/pholio/xaction/PholioImageTransactionType.php',
'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php',
'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php',
'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php',
'PholioMock' => 'applications/pholio/storage/PholioMock.php',
'PholioMockArchiveController' => 'applications/pholio/controller/PholioMockArchiveController.php',
'PholioMockAuthorHeraldField' => 'applications/pholio/herald/PholioMockAuthorHeraldField.php',
'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php',
'PholioMockDescriptionHeraldField' => 'applications/pholio/herald/PholioMockDescriptionHeraldField.php',
'PholioMockDescriptionTransaction' => 'applications/pholio/xaction/PholioMockDescriptionTransaction.php',
'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php',
'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php',
'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php',
'PholioMockFerretEngine' => 'applications/pholio/search/PholioMockFerretEngine.php',
'PholioMockFulltextEngine' => 'applications/pholio/search/PholioMockFulltextEngine.php',
'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php',
'PholioMockHasTaskRelationship' => 'applications/pholio/relationships/PholioMockHasTaskRelationship.php',
'PholioMockHeraldField' => 'applications/pholio/herald/PholioMockHeraldField.php',
'PholioMockHeraldFieldGroup' => 'applications/pholio/herald/PholioMockHeraldFieldGroup.php',
'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php',
'PholioMockInlineTransaction' => 'applications/pholio/xaction/PholioMockInlineTransaction.php',
'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php',
'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php',
'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php',
'PholioMockNameTransaction' => 'applications/pholio/xaction/PholioMockNameTransaction.php',
'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php',
'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php',
'PholioMockRelationship' => 'applications/pholio/relationships/PholioMockRelationship.php',
'PholioMockRelationshipSource' => 'applications/search/relationship/PholioMockRelationshipSource.php',
'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php',
'PholioMockStatusTransaction' => 'applications/pholio/xaction/PholioMockStatusTransaction.php',
'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php',
'PholioMockTransactionType' => 'applications/pholio/xaction/PholioMockTransactionType.php',
'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php',
'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php',
'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php',
'PholioSchemaSpec' => 'applications/pholio/storage/PholioSchemaSpec.php',
'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php',
'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php',
'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php',
'PholioTransactionType' => 'applications/pholio/xaction/PholioTransactionType.php',
'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php',
'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php',
'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php',
'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php',
'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php',
'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php',
'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php',
'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php',
'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php',
'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php',
'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php',
'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php',
'PhortuneAccountManagerController' => 'applications/phortune/controller/account/PhortuneAccountManagerController.php',
'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php',
'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php',
'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php',
'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php',
'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php',
'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php',
'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartAcceptController' => 'applications/phortune/controller/cart/PhortuneCartAcceptController.php',
'PhortuneCartCancelController' => 'applications/phortune/controller/cart/PhortuneCartCancelController.php',
'PhortuneCartCheckoutController' => 'applications/phortune/controller/cart/PhortuneCartCheckoutController.php',
'PhortuneCartController' => 'applications/phortune/controller/cart/PhortuneCartController.php',
'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php',
'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
'PhortuneCartListController' => 'applications/phortune/controller/cart/PhortuneCartListController.php',
'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php',
'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
'PhortuneCartTransaction' => 'applications/phortune/storage/PhortuneCartTransaction.php',
'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php',
'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php',
'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php',
'PhortuneChargeTableView' => 'applications/phortune/view/PhortuneChargeTableView.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
'PhortuneMerchantAddManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php',
'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php',
'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php',
'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php',
'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php',
'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php',
'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php',
'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php',
'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php',
'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php',
'PhortuneMerchantManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagerController.php',
'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php',
'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php',
'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php',
'PhortuneMerchantProfileController' => 'applications/phortune/controller/merchant/PhortuneMerchantProfileController.php',
'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php',
'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php',
'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php',
'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php',
'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php',
'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php',
'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php',
'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php',
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php',
'PhortuneProductListController' => 'applications/phortune/controller/product/PhortuneProductListController.php',
'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php',
'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
'PhortuneProductViewController' => 'applications/phortune/controller/product/PhortuneProductViewController.php',
'PhortuneProviderActionController' => 'applications/phortune/controller/provider/PhortuneProviderActionController.php',
'PhortuneProviderDisableController' => 'applications/phortune/controller/provider/PhortuneProviderDisableController.php',
'PhortuneProviderEditController' => 'applications/phortune/controller/provider/PhortuneProviderEditController.php',
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php',
'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php',
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
'PhragmentCanCreateCapability' => 'applications/phragment/capability/PhragmentCanCreateCapability.php',
'PhragmentConduitAPIMethod' => 'applications/phragment/conduit/PhragmentConduitAPIMethod.php',
'PhragmentController' => 'applications/phragment/controller/PhragmentController.php',
'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php',
'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php',
'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php',
'PhragmentFragmentPHIDType' => 'applications/phragment/phid/PhragmentFragmentPHIDType.php',
'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php',
'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php',
'PhragmentFragmentVersionPHIDType' => 'applications/phragment/phid/PhragmentFragmentVersionPHIDType.php',
'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php',
'PhragmentGetPatchConduitAPIMethod' => 'applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php',
'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php',
'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php',
'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php',
'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php',
'PhragmentQueryFragmentsConduitAPIMethod' => 'applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php',
'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php',
'PhragmentSchemaSpec' => 'applications/phragment/storage/PhragmentSchemaSpec.php',
'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php',
'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php',
'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php',
'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php',
'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php',
'PhragmentSnapshotPHIDType' => 'applications/phragment/phid/PhragmentSnapshotPHIDType.php',
'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php',
'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php',
'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php',
'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php',
'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php',
'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php',
'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php',
'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php',
'PhrequentCurtainExtension' => 'applications/phrequent/engineextension/PhrequentCurtainExtension.php',
'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php',
'PhrequentPushConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php',
'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
'PhrequentTimeSlices' => 'applications/phrequent/storage/PhrequentTimeSlices.php',
'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
'PhrequentTrackingConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php',
'PhrequentTrackingEditor' => 'applications/phrequent/editor/PhrequentTrackingEditor.php',
'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php',
'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php',
'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
'PhrictionConduitAPIMethod' => 'applications/phriction/conduit/PhrictionConduitAPIMethod.php',
'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php',
'PhrictionContentPHIDType' => 'applications/phriction/phid/PhrictionContentPHIDType.php',
'PhrictionContentQuery' => 'applications/phriction/query/PhrictionContentQuery.php',
'PhrictionContentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php',
'PhrictionContentSearchEngine' => 'applications/phriction/query/PhrictionContentSearchEngine.php',
'PhrictionContentSearchEngineAttachment' => 'applications/phriction/engineextension/PhrictionContentSearchEngineAttachment.php',
'PhrictionController' => 'applications/phriction/controller/PhrictionController.php',
'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php',
'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php',
'PhrictionDatasourceEngineExtension' => 'applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php',
'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php',
'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php',
'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php',
'PhrictionDocumentAuthorHeraldField' => 'applications/phriction/herald/PhrictionDocumentAuthorHeraldField.php',
'PhrictionDocumentContentHeraldField' => 'applications/phriction/herald/PhrictionDocumentContentHeraldField.php',
'PhrictionDocumentContentTransaction' => 'applications/phriction/xaction/PhrictionDocumentContentTransaction.php',
'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php',
'PhrictionDocumentDatasource' => 'applications/phriction/typeahead/PhrictionDocumentDatasource.php',
'PhrictionDocumentDeleteTransaction' => 'applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php',
+ 'PhrictionDocumentDraftTransaction' => 'applications/phriction/xaction/PhrictionDocumentDraftTransaction.php',
+ 'PhrictionDocumentEditEngine' => 'applications/phriction/editor/PhrictionDocumentEditEngine.php',
+ 'PhrictionDocumentEditTransaction' => 'applications/phriction/xaction/PhrictionDocumentEditTransaction.php',
'PhrictionDocumentFerretEngine' => 'applications/phriction/search/PhrictionDocumentFerretEngine.php',
'PhrictionDocumentFulltextEngine' => 'applications/phriction/search/PhrictionDocumentFulltextEngine.php',
'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php',
'PhrictionDocumentHeraldField' => 'applications/phriction/herald/PhrictionDocumentHeraldField.php',
'PhrictionDocumentHeraldFieldGroup' => 'applications/phriction/herald/PhrictionDocumentHeraldFieldGroup.php',
'PhrictionDocumentMoveAwayTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php',
'PhrictionDocumentMoveToTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php',
'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php',
'PhrictionDocumentPathHeraldField' => 'applications/phriction/herald/PhrictionDocumentPathHeraldField.php',
+ 'PhrictionDocumentPolicyCodex' => 'applications/phriction/codex/PhrictionDocumentPolicyCodex.php',
+ 'PhrictionDocumentPublishTransaction' => 'applications/phriction/xaction/PhrictionDocumentPublishTransaction.php',
'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php',
'PhrictionDocumentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php',
'PhrictionDocumentSearchEngine' => 'applications/phriction/query/PhrictionDocumentSearchEngine.php',
'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php',
'PhrictionDocumentTitleHeraldField' => 'applications/phriction/herald/PhrictionDocumentTitleHeraldField.php',
'PhrictionDocumentTitleTransaction' => 'applications/phriction/xaction/PhrictionDocumentTitleTransaction.php',
'PhrictionDocumentTransactionType' => 'applications/phriction/xaction/PhrictionDocumentTransactionType.php',
+ 'PhrictionDocumentVersionTransaction' => 'applications/phriction/xaction/PhrictionDocumentVersionTransaction.php',
'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
+ 'PhrictionEditEngineController' => 'applications/phriction/controller/PhrictionEditEngineController.php',
'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
'PhrictionMarkupPreviewController' => 'applications/phriction/controller/PhrictionMarkupPreviewController.php',
'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
+ 'PhrictionPublishController' => 'applications/phriction/controller/PhrictionPublishController.php',
'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php',
'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php',
'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php',
'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php',
'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php',
'PonderAnswerContentTransaction' => 'applications/ponder/xaction/PonderAnswerContentTransaction.php',
'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php',
'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php',
'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php',
'PonderAnswerMailReceiver' => 'applications/ponder/mail/PonderAnswerMailReceiver.php',
'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php',
'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php',
'PonderAnswerQuestionIDTransaction' => 'applications/ponder/xaction/PonderAnswerQuestionIDTransaction.php',
'PonderAnswerReplyHandler' => 'applications/ponder/mail/PonderAnswerReplyHandler.php',
'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php',
'PonderAnswerStatus' => 'applications/ponder/constants/PonderAnswerStatus.php',
'PonderAnswerStatusTransaction' => 'applications/ponder/xaction/PonderAnswerStatusTransaction.php',
'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php',
'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php',
'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php',
'PonderAnswerTransactionType' => 'applications/ponder/xaction/PonderAnswerTransactionType.php',
'PonderAnswerView' => 'applications/ponder/view/PonderAnswerView.php',
'PonderConstants' => 'applications/ponder/constants/PonderConstants.php',
'PonderController' => 'applications/ponder/controller/PonderController.php',
'PonderDAO' => 'applications/ponder/storage/PonderDAO.php',
'PonderDefaultViewCapability' => 'applications/ponder/capability/PonderDefaultViewCapability.php',
'PonderEditor' => 'applications/ponder/editor/PonderEditor.php',
'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php',
'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php',
'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
'PonderQuestionAnswerTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerTransaction.php',
'PonderQuestionAnswerWikiTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php',
'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php',
'PonderQuestionContentTransaction' => 'applications/ponder/xaction/PonderQuestionContentTransaction.php',
'PonderQuestionCreateMailReceiver' => 'applications/ponder/mail/PonderQuestionCreateMailReceiver.php',
'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php',
'PonderQuestionEditEngine' => 'applications/ponder/editor/PonderQuestionEditEngine.php',
'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php',
'PonderQuestionFerretEngine' => 'applications/ponder/search/PonderQuestionFerretEngine.php',
'PonderQuestionFulltextEngine' => 'applications/ponder/search/PonderQuestionFulltextEngine.php',
'PonderQuestionHistoryController' => 'applications/ponder/controller/PonderQuestionHistoryController.php',
'PonderQuestionListController' => 'applications/ponder/controller/PonderQuestionListController.php',
'PonderQuestionMailReceiver' => 'applications/ponder/mail/PonderQuestionMailReceiver.php',
'PonderQuestionPHIDType' => 'applications/ponder/phid/PonderQuestionPHIDType.php',
'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php',
'PonderQuestionReplyHandler' => 'applications/ponder/mail/PonderQuestionReplyHandler.php',
'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php',
'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php',
'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php',
'PonderQuestionStatusTransaction' => 'applications/ponder/xaction/PonderQuestionStatusTransaction.php',
'PonderQuestionTitleTransaction' => 'applications/ponder/xaction/PonderQuestionTitleTransaction.php',
'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php',
'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php',
'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php',
'PonderQuestionTransactionType' => 'applications/ponder/xaction/PonderQuestionTransactionType.php',
'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php',
'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php',
'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php',
'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php',
'ProjectColumnSearchConduitAPIMethod' => 'applications/project/conduit/ProjectColumnSearchConduitAPIMethod.php',
'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
'ProjectDatasourceEngineExtension' => 'applications/project/engineextension/ProjectDatasourceEngineExtension.php',
'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php',
'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php',
'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php',
'ProjectEditConduitAPIMethod' => 'applications/project/conduit/ProjectEditConduitAPIMethod.php',
'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php',
'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php',
'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php',
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php',
'ReleephBranchController' => 'applications/releeph/controller/branch/ReleephBranchController.php',
'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php',
'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php',
'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php',
'ReleephBranchHistoryController' => 'applications/releeph/controller/branch/ReleephBranchHistoryController.php',
'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php',
'ReleephBranchPHIDType' => 'applications/releeph/phid/ReleephBranchPHIDType.php',
'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php',
'ReleephBranchQuery' => 'applications/releeph/query/ReleephBranchQuery.php',
'ReleephBranchSearchEngine' => 'applications/releeph/query/ReleephBranchSearchEngine.php',
'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php',
'ReleephBranchTransaction' => 'applications/releeph/storage/ReleephBranchTransaction.php',
'ReleephBranchTransactionQuery' => 'applications/releeph/query/ReleephBranchTransactionQuery.php',
'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php',
'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php',
'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php',
'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php',
'ReleephConduitAPIMethod' => 'applications/releeph/conduit/ReleephConduitAPIMethod.php',
'ReleephController' => 'applications/releeph/controller/ReleephController.php',
'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php',
'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php',
'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php',
'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php',
'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php',
'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php',
'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php',
'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php',
'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php',
'ReleephGetBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php',
'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php',
'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php',
'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php',
'ReleephProductActionController' => 'applications/releeph/controller/product/ReleephProductActionController.php',
'ReleephProductController' => 'applications/releeph/controller/product/ReleephProductController.php',
'ReleephProductCreateController' => 'applications/releeph/controller/product/ReleephProductCreateController.php',
'ReleephProductEditController' => 'applications/releeph/controller/product/ReleephProductEditController.php',
'ReleephProductEditor' => 'applications/releeph/editor/ReleephProductEditor.php',
'ReleephProductHistoryController' => 'applications/releeph/controller/product/ReleephProductHistoryController.php',
'ReleephProductListController' => 'applications/releeph/controller/product/ReleephProductListController.php',
'ReleephProductPHIDType' => 'applications/releeph/phid/ReleephProductPHIDType.php',
'ReleephProductQuery' => 'applications/releeph/query/ReleephProductQuery.php',
'ReleephProductSearchEngine' => 'applications/releeph/query/ReleephProductSearchEngine.php',
'ReleephProductTransaction' => 'applications/releeph/storage/ReleephProductTransaction.php',
'ReleephProductTransactionQuery' => 'applications/releeph/query/ReleephProductTransactionQuery.php',
'ReleephProductViewController' => 'applications/releeph/controller/product/ReleephProductViewController.php',
'ReleephProject' => 'applications/releeph/storage/ReleephProject.php',
'ReleephQueryBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryBranchesConduitAPIMethod.php',
'ReleephQueryProductsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryProductsConduitAPIMethod.php',
'ReleephQueryRequestsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php',
'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php',
'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php',
'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php',
'ReleephRequestCommentController' => 'applications/releeph/controller/request/ReleephRequestCommentController.php',
'ReleephRequestConduitAPIMethod' => 'applications/releeph/conduit/ReleephRequestConduitAPIMethod.php',
'ReleephRequestController' => 'applications/releeph/controller/request/ReleephRequestController.php',
'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php',
'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php',
'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php',
'ReleephRequestPHIDType' => 'applications/releeph/phid/ReleephRequestPHIDType.php',
'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php',
'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php',
'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php',
'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php',
'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php',
'ReleephRequestTransactionComment' => 'applications/releeph/storage/ReleephRequestTransactionComment.php',
'ReleephRequestTransactionQuery' => 'applications/releeph/query/ReleephRequestTransactionQuery.php',
'ReleephRequestTransactionalEditor' => 'applications/releeph/editor/ReleephRequestTransactionalEditor.php',
'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php',
'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php',
'ReleephRequestView' => 'applications/releeph/view/ReleephRequestView.php',
'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php',
'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php',
'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php',
'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php',
'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
'ReleephWorkCanPushConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php',
'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php',
'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php',
'ReleephWorkGetBranchConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php',
'ReleephWorkGetCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php',
'ReleephWorkNextRequestConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php',
'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php',
'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php',
'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php',
'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php',
'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php',
'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php',
'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php',
'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php',
'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php',
'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php',
'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php',
'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php',
'TokenQueryConduitAPIMethod' => 'applications/tokens/conduit/TokenQueryConduitAPIMethod.php',
'TransactionSearchConduitAPIMethod' => 'applications/transactions/conduit/TransactionSearchConduitAPIMethod.php',
'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php',
'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php',
+ 'UserEditConduitAPIMethod' => 'applications/people/conduit/UserEditConduitAPIMethod.php',
'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php',
'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php',
'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php',
'UserSearchConduitAPIMethod' => 'applications/people/conduit/UserSearchConduitAPIMethod.php',
'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php',
),
'function' => array(
'celerity_generate_unique_node_id' => 'applications/celerity/api.php',
'celerity_get_resource_uri' => 'applications/celerity/api.php',
'javelin_tag' => 'infrastructure/javelin/markup.php',
'phabricator_date' => 'view/viewutils.php',
'phabricator_datetime' => 'view/viewutils.php',
'phabricator_datetimezone' => 'view/viewutils.php',
'phabricator_form' => 'infrastructure/javelin/markup.php',
'phabricator_format_local_time' => 'view/viewutils.php',
'phabricator_relative_date' => 'view/viewutils.php',
'phabricator_time' => 'view/viewutils.php',
'phid_get_subtype' => 'applications/phid/utils.php',
'phid_get_type' => 'applications/phid/utils.php',
'phid_group_by_type' => 'applications/phid/utils.php',
'require_celerity_resource' => 'applications/celerity/api.php',
),
'xmap' => array(
'AlmanacAddress' => 'Phobject',
'AlmanacBinding' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacBindingDeletePropertyTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingDisableController' => 'AlmanacServiceController',
'AlmanacBindingDisableTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacBindingEditController' => 'AlmanacServiceController',
'AlmanacBindingEditEngine' => 'PhabricatorEditEngine',
'AlmanacBindingEditor' => 'AlmanacEditor',
'AlmanacBindingInterfaceTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingPHIDType' => 'PhabricatorPHIDType',
'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacBindingQuery' => 'AlmanacQuery',
'AlmanacBindingSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacBindingSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacBindingServiceTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingSetPropertyTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingTableView' => 'AphrontView',
'AlmanacBindingTransaction' => 'AlmanacModularTransaction',
'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacBindingTransactionType' => 'AlmanacTransactionType',
'AlmanacBindingViewController' => 'AlmanacServiceController',
'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment',
'AlmanacCacheEngineExtension' => 'PhabricatorCacheEngineExtension',
'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType',
'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType',
'AlmanacClusterServiceType' => 'AlmanacServiceType',
'AlmanacConsoleController' => 'AlmanacController',
'AlmanacController' => 'PhabricatorController',
'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCustomServiceType' => 'AlmanacServiceType',
'AlmanacDAO' => 'PhabricatorLiskDAO',
'AlmanacDeletePropertyEditField' => 'PhabricatorEditField',
'AlmanacDeletePropertyEditType' => 'PhabricatorEditType',
'AlmanacDevice' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSSHPublicKeyInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacDeviceController' => 'AlmanacController',
'AlmanacDeviceDeletePropertyTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacDeviceEditController' => 'AlmanacDeviceController',
'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine',
'AlmanacDeviceEditor' => 'AlmanacEditor',
'AlmanacDeviceListController' => 'AlmanacDeviceController',
'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacDeviceNameTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacDeviceQuery' => 'AlmanacQuery',
'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacDeviceSetPropertyTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDeviceTransaction' => 'AlmanacModularTransaction',
'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacDeviceTransactionType' => 'AlmanacTransactionType',
'AlmanacDeviceViewController' => 'AlmanacDeviceController',
'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType',
'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacInterface' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacInterfaceAddressTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController',
'AlmanacInterfaceDestroyTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceDeviceTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacInterfaceEditController' => 'AlmanacDeviceController',
'AlmanacInterfaceEditEngine' => 'PhabricatorEditEngine',
'AlmanacInterfaceEditor' => 'AlmanacEditor',
'AlmanacInterfaceNetworkTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
'AlmanacInterfacePortTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceQuery' => 'AlmanacQuery',
'AlmanacInterfaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacInterfaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacInterfaceTableView' => 'AphrontView',
'AlmanacInterfaceTransaction' => 'AlmanacModularTransaction',
'AlmanacInterfaceTransactionType' => 'AlmanacTransactionType',
'AlmanacKeys' => 'Phobject',
'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
'AlmanacModularTransaction' => 'PhabricatorModularTransaction',
'AlmanacNames' => 'Phobject',
'AlmanacNamesTestCase' => 'PhabricatorTestCase',
'AlmanacNamespace' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacNamespaceController' => 'AlmanacController',
'AlmanacNamespaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacNamespaceEditController' => 'AlmanacNamespaceController',
'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine',
'AlmanacNamespaceEditor' => 'AlmanacEditor',
'AlmanacNamespaceListController' => 'AlmanacNamespaceController',
'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNamespaceNameTransaction' => 'AlmanacNamespaceTransactionType',
'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType',
'AlmanacNamespaceQuery' => 'AlmanacQuery',
'AlmanacNamespaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNamespaceTransaction' => 'AlmanacModularTransaction',
'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNamespaceTransactionType' => 'AlmanacTransactionType',
'AlmanacNamespaceViewController' => 'AlmanacNamespaceController',
'AlmanacNetwork' => array(
'AlmanacDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacNetworkController' => 'AlmanacController',
'AlmanacNetworkEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacNetworkEditController' => 'AlmanacNetworkController',
'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine',
'AlmanacNetworkEditor' => 'AlmanacEditor',
'AlmanacNetworkListController' => 'AlmanacNetworkController',
'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNetworkNameTransaction' => 'AlmanacNetworkTransactionType',
'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType',
'AlmanacNetworkQuery' => 'AlmanacQuery',
'AlmanacNetworkSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNetworkTransaction' => 'AlmanacModularTransaction',
'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNetworkTransactionType' => 'AlmanacTransactionType',
'AlmanacNetworkViewController' => 'AlmanacNetworkController',
'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'AlmanacPropertiesEditEngineExtension' => 'PhabricatorEditEngineExtension',
'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment',
'AlmanacProperty' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
),
'AlmanacPropertyController' => 'AlmanacController',
'AlmanacPropertyDeleteController' => 'AlmanacPropertyController',
'AlmanacPropertyEditController' => 'AlmanacPropertyController',
'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine',
'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'AlmanacService' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacServiceController' => 'AlmanacController',
'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacServiceDeletePropertyTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacServiceEditController' => 'AlmanacServiceController',
'AlmanacServiceEditEngine' => 'PhabricatorEditEngine',
'AlmanacServiceEditor' => 'AlmanacEditor',
'AlmanacServiceListController' => 'AlmanacServiceController',
'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacServiceNameTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServicePHIDType' => 'PhabricatorPHIDType',
'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacServiceQuery' => 'AlmanacQuery',
'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacServiceSetPropertyTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceTransaction' => 'AlmanacModularTransaction',
'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacServiceTransactionType' => 'AlmanacTransactionType',
'AlmanacServiceType' => 'Phobject',
'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase',
'AlmanacServiceTypeTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceViewController' => 'AlmanacServiceController',
'AlmanacSetPropertyEditField' => 'PhabricatorEditField',
'AlmanacSetPropertyEditType' => 'PhabricatorEditType',
'AlmanacTransactionType' => 'PhabricatorModularTransactionType',
'AphlictDropdownDataQuery' => 'Phobject',
'Aphront304Response' => 'AphrontResponse',
'Aphront400Response' => 'AphrontResponse',
'Aphront403Response' => 'AphrontHTMLResponse',
'Aphront404Response' => 'AphrontHTMLResponse',
'AphrontAjaxResponse' => 'AphrontResponse',
'AphrontApplicationConfiguration' => 'Phobject',
'AphrontBarView' => 'AphrontView',
'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontCalendarEventView' => 'AphrontView',
'AphrontController' => 'Phobject',
'AphrontCursorPagerView' => 'AphrontView',
'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => array(
'AphrontView',
'AphrontResponseProducerInterface',
),
'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontException' => 'Exception',
'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormDateControl' => 'AphrontFormControl',
'AphrontFormDateControlValue' => 'Phobject',
'AphrontFormDividerControl' => 'AphrontFormControl',
'AphrontFormFileControl' => 'AphrontFormControl',
'AphrontFormHandlesControl' => 'AphrontFormControl',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',
'AphrontFormPolicyControl' => 'AphrontFormControl',
'AphrontFormRadioButtonControl' => 'AphrontFormControl',
'AphrontFormRecaptchaControl' => 'AphrontFormControl',
'AphrontFormSelectControl' => 'AphrontFormControl',
'AphrontFormStaticControl' => 'AphrontFormControl',
'AphrontFormSubmitControl' => 'AphrontFormControl',
'AphrontFormTextAreaControl' => 'AphrontFormControl',
'AphrontFormTextControl' => 'AphrontFormControl',
'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
'AphrontFormTokenizerControl' => 'AphrontFormControl',
'AphrontFormTypeaheadControl' => 'AphrontFormControl',
'AphrontFormView' => 'AphrontView',
'AphrontGlyphBarView' => 'AphrontBarView',
'AphrontHTMLResponse' => 'AphrontResponse',
'AphrontHTTPParameterType' => 'Phobject',
'AphrontHTTPProxyResponse' => 'AphrontResponse',
'AphrontHTTPSink' => 'Phobject',
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
'AphrontJSONResponse' => 'AphrontResponse',
'AphrontJavelinView' => 'AphrontView',
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
'AphrontListFilterView' => 'AphrontView',
'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontMalformedRequestException' => 'AphrontException',
'AphrontMoreView' => 'AphrontView',
'AphrontMultiColumnView' => 'AphrontView',
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontNullView' => 'AphrontView',
'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
'AphrontPageView' => 'AphrontView',
'AphrontPlainTextResponse' => 'AphrontResponse',
'AphrontProgressBarView' => 'AphrontBarView',
'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontProxyResponse' => array(
'AphrontResponse',
'AphrontResponseProducerInterface',
),
'AphrontRedirectResponse' => 'AphrontResponse',
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
'AphrontReloadResponse' => 'AphrontRedirectResponse',
'AphrontRequest' => 'Phobject',
'AphrontRequestExceptionHandler' => 'Phobject',
'AphrontRequestTestCase' => 'PhabricatorTestCase',
'AphrontResponse' => 'Phobject',
'AphrontRoutingMap' => 'Phobject',
'AphrontRoutingResult' => 'Phobject',
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontSideNavFilterView' => 'AphrontView',
'AphrontSite' => 'Phobject',
'AphrontStackTraceView' => 'AphrontView',
'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
'AphrontStringHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontStringListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontTableView' => 'AphrontView',
'AphrontTagView' => 'AphrontView',
'AphrontTokenizerTemplateView' => 'AphrontView',
'AphrontTypeaheadTemplateView' => 'AphrontView',
'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse',
'AphrontUserListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontView' => array(
'Phobject',
'PhutilSafeHTMLProducerInterface',
),
'AphrontWebpageResponse' => 'AphrontHTMLResponse',
'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
'AuditConduitAPIMethod' => 'ConduitAPIMethod',
'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability',
'BulkParameterType' => 'Phobject',
'BulkPointsParameterType' => 'BulkParameterType',
'BulkRemarkupParameterType' => 'BulkParameterType',
'BulkSelectParameterType' => 'BulkParameterType',
'BulkStringParameterType' => 'BulkParameterType',
'BulkTokenizerParameterType' => 'BulkParameterType',
'CalendarTimeUtil' => 'Phobject',
'CalendarTimeUtilTestCase' => 'PhabricatorTestCase',
'CelerityAPI' => 'Phobject',
'CelerityDarkModePostprocessor' => 'CelerityPostprocessor',
'CelerityDefaultPostprocessor' => 'CelerityPostprocessor',
'CelerityHighContrastPostprocessor' => 'CelerityPostprocessor',
'CelerityLargeFontPostprocessor' => 'CelerityPostprocessor',
'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow',
'CelerityManagementSyntaxWorkflow' => 'CelerityManagementWorkflow',
'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow',
'CelerityPhabricatorResourceController' => 'CelerityResourceController',
'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk',
'CelerityPhysicalResources' => 'CelerityResources',
'CelerityPhysicalResourcesTestCase' => 'PhabricatorTestCase',
'CelerityPostprocessor' => 'Phobject',
'CelerityPostprocessorTestCase' => 'PhabricatorTestCase',
'CelerityRedGreenPostprocessor' => 'CelerityPostprocessor',
'CelerityResourceController' => 'PhabricatorController',
'CelerityResourceGraph' => 'AbstractDirectedGraph',
'CelerityResourceMap' => 'Phobject',
'CelerityResourceMapGenerator' => 'Phobject',
'CelerityResourceTransformer' => 'Phobject',
'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase',
'CelerityResources' => 'Phobject',
'CelerityResourcesOnDisk' => 'CelerityPhysicalResources',
'CeleritySpriteGenerator' => 'Phobject',
'CelerityStaticResourceResponse' => 'Phobject',
'ChatLogConduitAPIMethod' => 'ConduitAPIMethod',
'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod',
'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod',
'ConduitAPIMethod' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'ConduitAPIMethodTestCase' => 'PhabricatorTestCase',
'ConduitAPIRequest' => 'Phobject',
'ConduitAPIResponse' => 'Phobject',
'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException',
'ConduitBoolParameterType' => 'ConduitParameterType',
'ConduitCall' => 'Phobject',
'ConduitCallTestCase' => 'PhabricatorTestCase',
'ConduitColumnsParameterType' => 'ConduitParameterType',
'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod',
+ 'ConduitConstantDescription' => 'Phobject',
'ConduitEpochParameterType' => 'ConduitParameterType',
'ConduitException' => 'Exception',
'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitIntListParameterType' => 'ConduitListParameterType',
'ConduitIntParameterType' => 'ConduitParameterType',
'ConduitListParameterType' => 'ConduitParameterType',
'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector',
'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException',
'ConduitMethodNotFoundException' => 'ConduitException',
'ConduitPHIDListParameterType' => 'ConduitListParameterType',
'ConduitPHIDParameterType' => 'ConduitParameterType',
'ConduitParameterType' => 'Phobject',
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitPointsParameterType' => 'ConduitParameterType',
'ConduitProjectListParameterType' => 'ConduitListParameterType',
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
'ConduitStringListParameterType' => 'ConduitListParameterType',
'ConduitStringParameterType' => 'ConduitParameterType',
'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'ConduitUserListParameterType' => 'ConduitListParameterType',
'ConduitUserParameterType' => 'ConduitParameterType',
'ConduitWildParameterType' => 'ConduitParameterType',
'ConpherenceColumnViewController' => 'ConpherenceController',
'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
'ConpherenceConstants' => 'Phobject',
'ConpherenceController' => 'PhabricatorController',
'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceDAO' => 'PhabricatorLiskDAO',
'ConpherenceDurableColumnView' => 'AphrontTagView',
'ConpherenceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ConpherenceEditEngine' => 'PhabricatorEditEngine',
'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceIndex' => 'ConpherenceDAO',
'ConpherenceLayoutView' => 'AphrontTagView',
'ConpherenceListController' => 'ConpherenceController',
'ConpherenceMenuItemView' => 'AphrontTagView',
'ConpherenceNotificationPanelController' => 'ConpherenceController',
'ConpherenceParticipant' => 'ConpherenceDAO',
'ConpherenceParticipantController' => 'ConpherenceController',
'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantView' => 'AphrontView',
'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
'ConpherenceRoomEditController' => 'ConpherenceController',
'ConpherenceRoomListController' => 'ConpherenceController',
'ConpherenceRoomPictureController' => 'ConpherenceController',
'ConpherenceRoomPreferencesController' => 'ConpherenceController',
'ConpherenceRoomSettings' => 'ConpherenceConstants',
'ConpherenceRoomTestCase' => 'ConpherenceTestCase',
'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ConpherenceTestCase' => 'PhabricatorTestCase',
'ConpherenceThread' => array(
'ConpherenceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'ConpherenceThreadDatasource' => 'PhabricatorTypeaheadDatasource',
'ConpherenceThreadDateMarkerTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'ConpherenceThreadListView' => 'AphrontView',
'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver',
'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule',
'ConpherenceThreadParticipantsTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadPictureTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ConpherenceThreadSearchController' => 'ConpherenceController',
'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ConpherenceThreadTitleNgrams' => 'PhabricatorSearchNgrams',
'ConpherenceThreadTitleTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadTopicTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadTransactionType' => 'PhabricatorModularTransactionType',
'ConpherenceTransaction' => 'PhabricatorModularTransaction',
'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ConpherenceTransactionRenderer' => 'Phobject',
'ConpherenceTransactionView' => 'AphrontView',
'ConpherenceUpdateActions' => 'ConpherenceConstants',
'ConpherenceUpdateController' => 'ConpherenceController',
'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceViewController' => 'ConpherenceController',
'CountdownEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'CountdownSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DarkConsoleController' => 'PhabricatorController',
'DarkConsoleCore' => 'Phobject',
'DarkConsoleDataController' => 'PhabricatorController',
'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin',
'DarkConsoleErrorLogPluginAPI' => 'Phobject',
'DarkConsoleEventPlugin' => 'DarkConsolePlugin',
'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener',
'DarkConsolePlugin' => 'Phobject',
'DarkConsoleRealtimePlugin' => 'DarkConsolePlugin',
'DarkConsoleRequestPlugin' => 'DarkConsolePlugin',
'DarkConsoleServicesPlugin' => 'DarkConsolePlugin',
'DarkConsoleStartupPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPluginAPI' => 'Phobject',
'DifferentialAction' => 'Phobject',
'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
'DifferentialAdjustmentMapTestCase' => 'PhutilTestCase',
'DifferentialAffectedPath' => 'DifferentialDAO',
'DifferentialAsanaRepresentationField' => 'DifferentialCustomField',
'DifferentialAuditorsCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialAuditorsField' => 'DifferentialStoredCustomField',
'DifferentialBlameRevisionCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
'DifferentialBlockHeraldAction' => 'HeraldAction',
'DifferentialBlockingReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialBranchField' => 'DifferentialCustomField',
'DifferentialBuildableEngine' => 'HarbormasterBuildableEngine',
'DifferentialChangeDetailMailView' => 'DifferentialMailView',
'DifferentialChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialChangeType' => 'Phobject',
'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField',
'DifferentialChangeset' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialChangesetDetailView' => 'AphrontView',
+ 'DifferentialChangesetEngine' => 'Phobject',
'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject',
'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetListController' => 'DifferentialController',
'DifferentialChangesetListView' => 'AphrontView',
'DifferentialChangesetOneUpMailRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetParser' => 'Phobject',
'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase',
'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialChangesetRenderer' => 'Phobject',
'DifferentialChangesetSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetViewController' => 'DifferentialController',
'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCommitMessageCustomField' => 'DifferentialCommitMessageField',
'DifferentialCommitMessageField' => 'Phobject',
'DifferentialCommitMessageFieldTestCase' => 'PhabricatorTestCase',
'DifferentialCommitMessageParser' => 'Phobject',
'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
'DifferentialCommitsField' => 'DifferentialCustomField',
+ 'DifferentialCommitsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialController' => 'PhabricatorController',
'DifferentialCoreCustomField' => 'DifferentialCustomField',
'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateMailReceiver' => 'PhabricatorMailReceiver',
'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCustomField' => 'PhabricatorCustomField',
'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser',
'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase',
'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser',
'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase',
'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'DifferentialDAO' => 'PhabricatorLiskDAO',
'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DifferentialDiff' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'DifferentialDiffAffectedFilesHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffAuthorHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffAuthorProjectsHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentAddedHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentRemovedHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffCreateController' => 'DifferentialController',
'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialDiffExtractionEngine' => 'Phobject',
'DifferentialDiffHeraldField' => 'HeraldField',
'DifferentialDiffHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
'DifferentialDiffPHIDType' => 'PhabricatorPHIDType',
'DifferentialDiffProperty' => 'DifferentialDAO',
'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialDiffRepositoryHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffRepositoryProjectsHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialDiffSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialDiffTestCase' => 'PhutilTestCase',
'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction',
'DifferentialDiffTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialDiffViewController' => 'DifferentialController',
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DifferentialDraftField' => 'DifferentialCoreCustomField',
'DifferentialExactUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialFieldParseException' => 'Exception',
'DifferentialFieldValidationException' => 'Exception',
'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetWorkingCopy' => 'Phobject',
'DifferentialGitSVNIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialHarbormasterField' => 'DifferentialCustomField',
'DifferentialHeraldStateReasons' => 'HeraldStateReasons',
'DifferentialHiddenComment' => 'DifferentialDAO',
'DifferentialHostField' => 'DifferentialCustomField',
'DifferentialHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'DifferentialHunk' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialHunkParser' => 'Phobject',
'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialHunkTestCase' => 'PhutilTestCase',
'DifferentialInlineComment' => array(
'Phobject',
'PhabricatorInlineCommentInterface',
),
'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController',
'DifferentialInlineCommentMailView' => 'DifferentialMailView',
'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
'DifferentialJIRAIssuesCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
'DifferentialLegacyQuery' => 'Phobject',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialHarbormasterField',
'DifferentialLintStatus' => 'Phobject',
'DifferentialLocalCommitsView' => 'AphrontView',
'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension',
'DifferentialMailView' => 'Phobject',
'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector',
'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialParseRenderTestCase' => 'PhabricatorTestCase',
'DifferentialPathField' => 'DifferentialCustomField',
'DifferentialProjectReviewersField' => 'DifferentialCustomField',
'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialRawDiffRenderer' => 'Phobject',
'DifferentialReleephRequestFieldSpecification' => 'Phobject',
'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'DifferentialRepositoryField' => 'DifferentialCoreCustomField',
'DifferentialRepositoryLookup' => 'Phobject',
'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField',
'DifferentialResponsibleDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevertPlanCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
'DifferentialReviewedByCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewer' => 'DifferentialDAO',
'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialReviewerStatus' => 'Phobject',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewersField' => 'DifferentialCoreCustomField',
'DifferentialReviewersHeraldAction' => 'HeraldAction',
'DifferentialReviewersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialReviewersView' => 'AphrontView',
'DifferentialRevision' => array(
'DifferentialDAO',
'PhabricatorTokenReceiverInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorFlaggableInterface',
'PhrequentTrackableInterface',
'HarbormasterBuildableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDraftInterface',
),
'DifferentialRevisionAbandonTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionAcceptTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionActionTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionAffectedFilesHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionAuthorHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionAuthorProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionBuildableTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionCloseDetailsController' => 'DifferentialController',
'DifferentialRevisionCloseTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionCommandeerTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionContentAddedHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionContentHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionContentRemovedHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionControlSystem' => 'Phobject',
'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionDraftEngine' => 'PhabricatorDraftEngine',
'DifferentialRevisionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DifferentialRevisionEditController' => 'DifferentialController',
'DifferentialRevisionEditEngine' => 'PhabricatorEditEngine',
'DifferentialRevisionFerretEngine' => 'PhabricatorFerretEngine',
'DifferentialRevisionFulltextEngine' => 'PhabricatorFulltextEngine',
'DifferentialRevisionGraph' => 'PhabricatorObjectGraph',
'DifferentialRevisionHasChildRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasCommitRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasParentRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasTaskRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHeraldField' => 'HeraldField',
'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialRevisionHoldDraftTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialRevisionInlineTransaction' => 'PhabricatorModularTransactionType',
'DifferentialRevisionInlinesController' => 'DifferentialController',
'DifferentialRevisionListController' => 'DifferentialController',
'DifferentialRevisionListView' => 'AphrontView',
'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
'DifferentialRevisionOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionOperationController' => 'DifferentialController',
'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType',
'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPlanChangesTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialRevisionReclaimTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRejectTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionRelationship' => 'PhabricatorObjectRelationship',
'DifferentialRevisionRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'DifferentialRevisionReopenTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionRequestReviewTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRequiredActionResultBucket' => 'DifferentialRevisionResultBucket',
'DifferentialRevisionResignTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionResultBucket' => 'PhabricatorSearchResultBucket',
'DifferentialRevisionReviewTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionReviewersHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionReviewersTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialRevisionStatus' => 'Phobject',
'DifferentialRevisionStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialRevisionStatusHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionStatusTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionSummaryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType',
+ 'DifferentialRevisionTestPlanHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTestPlanTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTitleTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType',
'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
'DifferentialRevisionUpdateTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionViewController' => 'DifferentialController',
'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionWrongStateTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialStoredCustomField' => 'DifferentialCustomField',
'DifferentialSubscribersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialSummaryCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialSummaryField' => 'DifferentialCoreCustomField',
'DifferentialTagsCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTasksCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTestPlanCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTestPlanField' => 'DifferentialCoreCustomField',
'DifferentialTitleCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTransaction' => 'PhabricatorModularTransaction',
'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment',
'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView',
'DifferentialUnitField' => 'DifferentialCustomField',
'DifferentialUnitStatus' => 'Phobject',
'DifferentialUnitTestResult' => 'Phobject',
'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionAuditorFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionAuditorsAddAuditorsHeraldAction' => 'DiffusionAuditorsHeraldAction',
'DiffusionAuditorsAddSelfHeraldAction' => 'DiffusionAuditorsHeraldAction',
'DiffusionAuditorsHeraldAction' => 'HeraldAction',
'DiffusionBlameConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBlameController' => 'DiffusionController',
'DiffusionBlameQuery' => 'DiffusionQuery',
'DiffusionBlockHeraldAction' => 'HeraldAction',
'DiffusionBranchListView' => 'DiffusionView',
'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBranchTableController' => 'DiffusionController',
'DiffusionBranchTableView' => 'DiffusionView',
'DiffusionBrowseController' => 'DiffusionController',
'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBrowseResultSet' => 'Phobject',
'DiffusionBrowseTableView' => 'DiffusionView',
'DiffusionBuildableEngine' => 'HarbormasterBuildableEngine',
'DiffusionCacheEngineExtension' => 'PhabricatorCacheEngineExtension',
'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionChangeController' => 'DiffusionController',
'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCloneController' => 'DiffusionController',
'DiffusionCloneURIView' => 'AphrontView',
'DiffusionCommandEngine' => 'Phobject',
'DiffusionCommandEngineTestCase' => 'PhabricatorTestCase',
'DiffusionCommitAcceptTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitActionTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField',
+ 'DiffusionCommitAuditStatus' => 'Phobject',
'DiffusionCommitAuditTransaction' => 'DiffusionCommitActionTransaction',
'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBranchesController' => 'DiffusionController',
'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBuildableTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitController' => 'DiffusionController',
'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffContentHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffContentRemovedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffEnormousHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDraftEngine' => 'PhabricatorDraftEngine',
'DiffusionCommitEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionCommitEditController' => 'DiffusionController',
'DiffusionCommitEditEngine' => 'PhabricatorEditEngine',
'DiffusionCommitFerretEngine' => 'PhabricatorFerretEngine',
'DiffusionCommitFulltextEngine' => 'PhabricatorFulltextEngine',
'DiffusionCommitHasPackageEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasRevisionRelationship' => 'DiffusionCommitRelationship',
'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasTaskRelationship' => 'DiffusionCommitRelationship',
'DiffusionCommitHash' => 'Phobject',
'DiffusionCommitHeraldField' => 'HeraldField',
'DiffusionCommitHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCommitHintQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DiffusionCommitHookEngine' => 'Phobject',
'DiffusionCommitHookRejectException' => 'Exception',
'DiffusionCommitListController' => 'DiffusionController',
'DiffusionCommitListView' => 'AphrontView',
'DiffusionCommitMergeHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitMessageHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageAuditHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageOwnerHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DiffusionCommitRef' => 'Phobject',
'DiffusionCommitRelationship' => 'PhabricatorObjectRelationship',
'DiffusionCommitRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase',
'DiffusionCommitRepositoryHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRepositoryProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRequiredActionResultBucket' => 'DiffusionCommitResultBucket',
'DiffusionCommitResignTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitResultBucket' => 'PhabricatorSearchResultBucket',
'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitReviewerHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionAcceptedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionReviewersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionSubscribersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitTagsController' => 'DiffusionController',
'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType',
'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCompareController' => 'DiffusionController',
'DiffusionConduitAPIMethod' => 'ConduitAPIMethod',
'DiffusionController' => 'PhabricatorController',
'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability',
'DiffusionDaemonLockException' => 'Exception',
'DiffusionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability',
'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DiffusionDiffController' => 'DiffusionController',
'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionDocumentController' => 'DiffusionController',
'DiffusionDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine',
'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DiffusionEmptyResultView' => 'DiffusionView',
'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionExternalController' => 'DiffusionController',
'DiffusionExternalSymbolQuery' => 'Phobject',
'DiffusionExternalSymbolsSource' => 'Phobject',
'DiffusionFileContentQuery' => 'DiffusionFileFutureQuery',
'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionFileFutureQuery' => 'DiffusionQuery',
'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGitBlameQuery' => 'DiffusionBlameQuery',
'DiffusionGitBranch' => 'Phobject',
'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
'DiffusionGitCommandEngine' => 'DiffusionCommandEngine',
'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitLFSResponse' => 'AphrontResponse',
'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitRequest' => 'DiffusionRequest',
'DiffusionGitResponse' => 'AphrontResponse',
'DiffusionGitSSHWorkflow' => array(
'DiffusionSSHWorkflow',
'DiffusionRepositoryClusterEngineLogInterface',
),
'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGraphController' => 'DiffusionController',
'DiffusionHistoryController' => 'DiffusionController',
'DiffusionHistoryListView' => 'DiffusionHistoryView',
'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionHistoryTableView' => 'DiffusionHistoryView',
'DiffusionHistoryView' => 'DiffusionView',
'DiffusionHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
+ 'DiffusionIdentityAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
+ 'DiffusionIdentityAssigneeEditField' => 'PhabricatorTokenizerEditField',
+ 'DiffusionIdentityAssigneeSearchField' => 'PhabricatorSearchTokenizerField',
+ 'DiffusionIdentityEditController' => 'DiffusionController',
+ 'DiffusionIdentityListController' => 'DiffusionController',
+ 'DiffusionIdentityUnassignedDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'DiffusionIdentityViewController' => 'DiffusionController',
'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController',
'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
+ 'DiffusionInternalAncestorsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionLastModifiedController' => 'DiffusionController',
'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionLintController' => 'DiffusionController',
'DiffusionLintCountQuery' => 'PhabricatorQuery',
'DiffusionLintSaveRunner' => 'Phobject',
'DiffusionLocalRepositoryFilter' => 'Phobject',
'DiffusionLogController' => 'DiffusionController',
'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelQuery' => 'Phobject',
'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery',
'DiffusionMercurialCommandEngine' => 'DiffusionCommandEngine',
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionMercurialFlagInjectionException' => 'Exception',
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionMercurialRequest' => 'DiffusionRequest',
'DiffusionMercurialResponse' => 'AphrontResponse',
'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel',
'DiffusionMercurialWireProtocol' => 'Phobject',
'DiffusionMercurialWireProtocolTests' => 'PhabricatorTestCase',
'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionPathChange' => 'Phobject',
'DiffusionPathChangeQuery' => 'Phobject',
'DiffusionPathCompleteController' => 'DiffusionController',
'DiffusionPathIDQuery' => 'Phobject',
'DiffusionPathQuery' => 'Phobject',
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
'DiffusionPathTreeController' => 'DiffusionController',
'DiffusionPathValidateController' => 'DiffusionController',
'DiffusionPatternSearchView' => 'DiffusionView',
'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffEnormousHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentHeraldField' => 'HeraldField',
'DiffusionPreCommitContentMergeHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentMessageHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPackageHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPackageOwnerHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRepositoryHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitRefChangeHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefHeraldField' => 'HeraldField',
'DiffusionPreCommitRefHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionPreCommitRefNameHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefPusherHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefPusherProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefRepositoryHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefTypeHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitUsesGitLFSHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPullEventGarbageCollector' => 'PhabricatorGarbageCollector',
'DiffusionPullLogListController' => 'DiffusionLogController',
'DiffusionPullLogListView' => 'AphrontView',
'DiffusionPullLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionPushCapability' => 'PhabricatorPolicyCapability',
'DiffusionPushEventViewController' => 'DiffusionLogController',
'DiffusionPushLogListController' => 'DiffusionLogController',
'DiffusionPushLogListView' => 'AphrontView',
'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionQuery' => 'PhabricatorQuery',
'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRawDiffQuery' => 'DiffusionFileFutureQuery',
'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionReadmeView' => 'DiffusionView',
'DiffusionRefDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionRefNotFoundException' => 'Exception',
'DiffusionRefTableController' => 'DiffusionController',
'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRenameHistoryQuery' => 'Phobject',
'DiffusionRepositoryActionsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryAutomationManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBasicsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBranchesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositoryClusterEngine' => 'Phobject',
'DiffusionRepositoryController' => 'DiffusionController',
'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionRepositoryDefaultController' => 'DiffusionController',
'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionRepositoryEditController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditEngine' => 'PhabricatorEditEngine',
'DiffusionRepositoryEditEnormousController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel',
+ 'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionRepositoryListController' => 'DiffusionController',
'DiffusionRepositoryManageController' => 'DiffusionController',
'DiffusionRepositoryManagePanelsController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryManagementPanel' => 'Phobject',
'DiffusionRepositoryPath' => 'Phobject',
'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryProfilePictureController' => 'DiffusionController',
'DiffusionRepositoryRef' => 'Phobject',
'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionRepositoryStagingManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryStorageManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySubversionManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySymbolsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryTag' => 'Phobject',
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryURICredentialController' => 'DiffusionController',
'DiffusionRepositoryURIDisableController' => 'DiffusionController',
'DiffusionRepositoryURIEditController' => 'DiffusionController',
'DiffusionRepositoryURIViewController' => 'DiffusionController',
'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DiffusionRequest' => 'Phobject',
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionResolveUserQuery' => 'Phobject',
'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionServeController' => 'DiffusionController',
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'DiffusionSetupException' => 'Exception',
'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine',
'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow',
'DiffusionSubversionWireProtocol' => 'Phobject',
'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase',
'DiffusionSvnBlameQuery' => 'DiffusionBlameQuery',
'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionSvnRequest' => 'DiffusionRequest',
'DiffusionSymbolController' => 'DiffusionController',
'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery',
'DiffusionTagListController' => 'DiffusionController',
'DiffusionTagListView' => 'DiffusionView',
'DiffusionTagTableView' => 'DiffusionView',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionURIEditEngine' => 'PhabricatorEditEngine',
'DiffusionURIEditor' => 'PhabricatorApplicationTransactionEditor',
'DiffusionURITestCase' => 'PhutilTestCase',
'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionView' => 'AphrontView',
'DivinerArticleAtomizer' => 'DivinerAtomizer',
'DivinerAtom' => 'Phobject',
'DivinerAtomCache' => 'DivinerDiskCache',
'DivinerAtomController' => 'DivinerController',
'DivinerAtomListController' => 'DivinerController',
'DivinerAtomPHIDType' => 'PhabricatorPHIDType',
'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerAtomRef' => 'Phobject',
'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
'DivinerAtomizer' => 'Phobject',
'DivinerBookController' => 'DivinerController',
'DivinerBookDatasource' => 'PhabricatorTypeaheadDatasource',
'DivinerBookEditController' => 'DivinerController',
'DivinerBookItemView' => 'AphrontTagView',
'DivinerBookPHIDType' => 'PhabricatorPHIDType',
'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerController' => 'PhabricatorController',
'DivinerDAO' => 'PhabricatorLiskDAO',
'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DivinerDefaultRenderer' => 'DivinerRenderer',
'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DivinerDiskCache' => 'Phobject',
'DivinerFileAtomizer' => 'DivinerAtomizer',
'DivinerFindController' => 'DivinerController',
'DivinerGenerateWorkflow' => 'DivinerWorkflow',
'DivinerLiveAtom' => 'DivinerDAO',
'DivinerLiveBook' => array(
'DivinerDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
),
'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor',
'DivinerLiveBookFulltextEngine' => 'PhabricatorFulltextEngine',
'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction',
'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DivinerLivePublisher' => 'DivinerPublisher',
'DivinerLiveSymbol' => array(
'DivinerDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
),
'DivinerLiveSymbolFulltextEngine' => 'PhabricatorFulltextEngine',
'DivinerMainController' => 'DivinerController',
'DivinerPHPAtomizer' => 'DivinerAtomizer',
'DivinerParameterTableView' => 'AphrontTagView',
'DivinerPublishCache' => 'DivinerDiskCache',
'DivinerPublisher' => 'Phobject',
'DivinerRenderer' => 'Phobject',
'DivinerReturnTableView' => 'AphrontTagView',
'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DivinerSectionView' => 'AphrontTagView',
'DivinerStaticPublisher' => 'DivinerPublisher',
'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
'DivinerWorkflow' => 'PhabricatorManagementWorkflow',
'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperBridge' => 'Phobject',
'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub',
'DoorkeeperBridgeGitHubUser' => 'DoorkeeperBridgeGitHub',
'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge',
'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase',
'DoorkeeperBridgedObjectCurtainExtension' => 'PHUICurtainExtension',
'DoorkeeperDAO' => 'PhabricatorLiskDAO',
'DoorkeeperExternalObject' => array(
'DoorkeeperDAO',
'PhabricatorPolicyInterface',
),
'DoorkeeperExternalObjectPHIDType' => 'PhabricatorPHIDType',
'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DoorkeeperFeedStoryPublisher' => 'Phobject',
'DoorkeeperFeedWorker' => 'FeedPushWorker',
'DoorkeeperImportEngine' => 'Phobject',
'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperMissingLinkException' => 'Exception',
'DoorkeeperObjectRef' => 'Phobject',
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DoorkeeperTagView' => 'AphrontView',
'DoorkeeperTagsController' => 'PhabricatorController',
'DrydockAcquiredBrokenResourceException' => 'Exception',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
'DrydockAuthorization' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
),
'DrydockAuthorizationAuthorizeController' => 'DrydockController',
'DrydockAuthorizationListController' => 'DrydockController',
'DrydockAuthorizationListView' => 'AphrontView',
'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType',
'DrydockAuthorizationQuery' => 'DrydockQuery',
'DrydockAuthorizationSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockAuthorizationViewController' => 'DrydockController',
'DrydockBlueprint' => array(
'DrydockDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorNgramsInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
),
'DrydockBlueprintController' => 'DrydockController',
'DrydockBlueprintCoreCustomField' => array(
'DrydockBlueprintCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'DrydockBlueprintCustomField' => 'PhabricatorCustomField',
'DrydockBlueprintDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockBlueprintDisableController' => 'DrydockBlueprintController',
'DrydockBlueprintDisableTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DrydockBlueprintEditController' => 'DrydockBlueprintController',
'DrydockBlueprintEditEngine' => 'PhabricatorEditEngine',
'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor',
'DrydockBlueprintImplementation' => 'Phobject',
'DrydockBlueprintImplementationTestCase' => 'PhabricatorTestCase',
'DrydockBlueprintListController' => 'DrydockBlueprintController',
'DrydockBlueprintNameNgrams' => 'PhabricatorSearchNgrams',
'DrydockBlueprintNameTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType',
'DrydockBlueprintQuery' => 'DrydockQuery',
'DrydockBlueprintSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockBlueprintTransaction' => 'PhabricatorModularTransaction',
'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DrydockBlueprintTransactionType' => 'PhabricatorModularTransactionType',
'DrydockBlueprintTypeTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintViewController' => 'DrydockBlueprintController',
'DrydockCommand' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockCommandError' => 'Phobject',
'DrydockCommandInterface' => 'DrydockInterface',
'DrydockCommandQuery' => 'DrydockQuery',
'DrydockConsoleController' => 'DrydockController',
'DrydockController' => 'PhabricatorController',
'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability',
'DrydockDAO' => 'PhabricatorLiskDAO',
'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DrydockFilesystemInterface' => 'DrydockInterface',
'DrydockInterface' => 'Phobject',
'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockLease' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockLeaseAcquiredLogType' => 'DrydockLogType',
'DrydockLeaseActivatedLogType' => 'DrydockLogType',
'DrydockLeaseActivationFailureLogType' => 'DrydockLogType',
'DrydockLeaseActivationYieldLogType' => 'DrydockLogType',
'DrydockLeaseAllocationFailureLogType' => 'DrydockLogType',
'DrydockLeaseController' => 'DrydockController',
'DrydockLeaseDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockLeaseDestroyedLogType' => 'DrydockLogType',
'DrydockLeaseListController' => 'DrydockLeaseController',
'DrydockLeaseListView' => 'AphrontView',
'DrydockLeaseNoAuthorizationsLogType' => 'DrydockLogType',
'DrydockLeaseNoBlueprintsLogType' => 'DrydockLogType',
'DrydockLeasePHIDType' => 'PhabricatorPHIDType',
'DrydockLeaseQuery' => 'DrydockQuery',
'DrydockLeaseQueuedLogType' => 'DrydockLogType',
'DrydockLeaseReacquireLogType' => 'DrydockLogType',
'DrydockLeaseReclaimLogType' => 'DrydockLogType',
'DrydockLeaseReleaseController' => 'DrydockLeaseController',
'DrydockLeaseReleasedLogType' => 'DrydockLogType',
'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockLeaseStatus' => 'PhabricatorObjectStatus',
'DrydockLeaseUpdateWorker' => 'DrydockWorker',
'DrydockLeaseViewController' => 'DrydockLeaseController',
'DrydockLeaseWaitingForResourcesLogType' => 'DrydockLogType',
'DrydockLog' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockLogController' => 'DrydockController',
'DrydockLogGarbageCollector' => 'PhabricatorGarbageCollector',
'DrydockLogListController' => 'DrydockLogController',
'DrydockLogListView' => 'AphrontView',
'DrydockLogQuery' => 'DrydockQuery',
'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockLogType' => 'Phobject',
'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReclaimWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'DrydockObjectAuthorizationView' => 'AphrontView',
'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DrydockRepositoryOperation' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockRepositoryOperationController' => 'DrydockController',
'DrydockRepositoryOperationDismissController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationListController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType',
'DrydockRepositoryOperationQuery' => 'DrydockQuery',
'DrydockRepositoryOperationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockRepositoryOperationStatusController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationStatusView' => 'AphrontView',
'DrydockRepositoryOperationType' => 'Phobject',
'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker',
'DrydockRepositoryOperationViewController' => 'DrydockRepositoryOperationController',
'DrydockResource' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockResourceActivationFailureLogType' => 'DrydockLogType',
'DrydockResourceActivationYieldLogType' => 'DrydockLogType',
'DrydockResourceAllocationFailureLogType' => 'DrydockLogType',
'DrydockResourceController' => 'DrydockController',
'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockResourceListController' => 'DrydockResourceController',
'DrydockResourceListView' => 'AphrontView',
'DrydockResourceLockException' => 'Exception',
'DrydockResourcePHIDType' => 'PhabricatorPHIDType',
'DrydockResourceQuery' => 'DrydockQuery',
'DrydockResourceReclaimLogType' => 'DrydockLogType',
'DrydockResourceReleaseController' => 'DrydockResourceController',
'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockResourceStatus' => 'PhabricatorObjectStatus',
'DrydockResourceUpdateWorker' => 'DrydockWorker',
'DrydockResourceViewController' => 'DrydockResourceController',
'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
'DrydockSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DrydockSlotLock' => 'DrydockDAO',
'DrydockSlotLockException' => 'Exception',
'DrydockSlotLockFailureLogType' => 'DrydockLogType',
'DrydockTestRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockWebrootInterface' => 'DrydockInterface',
'DrydockWorker' => 'PhabricatorWorker',
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod',
'FeedConduitAPIMethod' => 'ConduitAPIMethod',
'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedPublisherHTTPWorker' => 'FeedPushWorker',
'FeedPublisherWorker' => 'FeedPushWorker',
'FeedPushWorker' => 'PhabricatorWorker',
'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedStoryNotificationGarbageCollector' => 'PhabricatorGarbageCollector',
'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod',
'FileConduitAPIMethod' => 'ConduitAPIMethod',
'FileCreateMailReceiver' => 'PhabricatorMailReceiver',
'FileDeletionWorker' => 'PhabricatorWorker',
'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod',
'FileMailReceiver' => 'PhabricatorObjectMailReceiver',
'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod',
'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FileTypeIcon' => 'Phobject',
'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod',
'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FlagConduitAPIMethod' => 'ConduitAPIMethod',
'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod',
'FundBacker' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'FundBackerCart' => 'PhortuneCartImplementation',
'FundBackerEditor' => 'PhabricatorApplicationTransactionEditor',
'FundBackerListController' => 'FundController',
'FundBackerPHIDType' => 'PhabricatorPHIDType',
'FundBackerProduct' => 'PhortuneProductImplementation',
'FundBackerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundBackerRefundTransaction' => 'FundBackerTransactionType',
'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundBackerStatusTransaction' => 'FundBackerTransactionType',
'FundBackerTransaction' => 'PhabricatorModularTransaction',
'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundBackerTransactionType' => 'PhabricatorModularTransactionType',
'FundController' => 'PhabricatorController',
'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability',
'FundDAO' => 'PhabricatorLiskDAO',
'FundDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FundInitiative' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'FundInitiativeBackController' => 'FundController',
'FundInitiativeBackerTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeCloseController' => 'FundController',
'FundInitiativeDescriptionTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeEditController' => 'FundController',
'FundInitiativeEditEngine' => 'PhabricatorEditEngine',
'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor',
'FundInitiativeFerretEngine' => 'PhabricatorFerretEngine',
'FundInitiativeFulltextEngine' => 'PhabricatorFulltextEngine',
'FundInitiativeListController' => 'FundController',
'FundInitiativeMerchantTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeNameTransaction' => 'FundInitiativeTransactionType',
'FundInitiativePHIDType' => 'PhabricatorPHIDType',
'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundInitiativeRefundTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FundInitiativeRisksTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundInitiativeStatusTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeTransaction' => 'PhabricatorModularTransaction',
'FundInitiativeTransactionComment' => 'PhabricatorApplicationTransactionComment',
'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundInitiativeTransactionType' => 'PhabricatorModularTransactionType',
'FundInitiativeViewController' => 'FundController',
'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
+ 'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArtifact' => 'Phobject',
'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase',
'HarbormasterBuild' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildAbortedException' => 'Exception',
'HarbormasterBuildActionController' => 'HarbormasterController',
'HarbormasterBuildArcanistAutoplan' => 'HarbormasterBuildAutoplan',
'HarbormasterBuildArtifact' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildAutoplan' => 'Phobject',
'HarbormasterBuildCommand' => 'HarbormasterDAO',
'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildEngine' => 'Phobject',
'HarbormasterBuildFailureException' => 'Exception',
'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
'HarbormasterBuildInitiatorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'HarbormasterBuildLintMessage' => 'HarbormasterDAO',
'HarbormasterBuildListController' => 'HarbormasterController',
'HarbormasterBuildLog' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'HarbormasterBuildLogChunk' => 'HarbormasterDAO',
'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator',
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildLogRenderController' => 'HarbormasterController',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildLogView' => 'AphrontView',
'HarbormasterBuildLogViewController' => 'HarbormasterController',
'HarbormasterBuildMessage' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildPlan' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorNgramsInterface',
'PhabricatorProjectInterface',
),
'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine',
'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams',
'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildRequest' => 'Phobject',
'HarbormasterBuildSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildStatus' => 'Phobject',
'HarbormasterBuildStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildStep' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
),
'HarbormasterBuildStepCoreCustomField' => array(
'HarbormasterBuildStepCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'HarbormasterBuildStepCustomField' => 'PhabricatorCustomField',
'HarbormasterBuildStepEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildStepGroup' => 'Phobject',
'HarbormasterBuildStepImplementation' => 'Phobject',
'HarbormasterBuildStepImplementationTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildStepPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildStepTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildStepTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildTarget' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildUnitMessage' => 'HarbormasterDAO',
'HarbormasterBuildViewController' => 'HarbormasterController',
'HarbormasterBuildWorker' => 'HarbormasterWorker',
'HarbormasterBuildable' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'HarbormasterBuildableInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildableActionController' => 'HarbormasterController',
'HarbormasterBuildableEngine' => 'Phobject',
'HarbormasterBuildableListController' => 'HarbormasterController',
'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildableStatus' => 'Phobject',
'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildableViewController' => 'HarbormasterController',
'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterBuildkiteHookController' => 'HarbormasterController',
'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterCircleCIHookController' => 'HarbormasterController',
'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
+ 'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterController' => 'PhabricatorController',
'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterCreatePlansCapability' => 'PhabricatorPolicyCapability',
'HarbormasterDAO' => 'PhabricatorLiskDAO',
'HarbormasterDrydockBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterDrydockCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterDrydockLeaseArtifact' => 'HarbormasterArtifact',
'HarbormasterExecFuture' => 'Future',
'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterFileArtifact' => 'HarbormasterArtifact',
'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact',
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView',
'HarbormasterLogWorker' => 'HarbormasterWorker',
'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementPublishWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRebuildLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterPlanController' => 'HarbormasterController',
'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
'HarbormasterPlanEditController' => 'HarbormasterPlanController',
'HarbormasterPlanListController' => 'HarbormasterPlanController',
'HarbormasterPlanRunController' => 'HarbormasterPlanController',
'HarbormasterPlanViewController' => 'HarbormasterPlanController',
'HarbormasterPrototypeBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterQueryAutotargetsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction',
'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HarbormasterScratchTable' => 'HarbormasterDAO',
'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterStepAddController' => 'HarbormasterPlanController',
'HarbormasterStepDeleteController' => 'HarbormasterPlanController',
'HarbormasterStepEditController' => 'HarbormasterPlanController',
'HarbormasterStepViewController' => 'HarbormasterPlanController',
'HarbormasterTargetEngine' => 'Phobject',
'HarbormasterTargetWorker' => 'HarbormasterWorker',
'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
'HarbormasterUIEventListener' => 'PhabricatorEventListener',
'HarbormasterURIArtifact' => 'HarbormasterArtifact',
'HarbormasterUnitMessageListController' => 'HarbormasterController',
'HarbormasterUnitMessageViewController' => 'HarbormasterController',
'HarbormasterUnitPropertyView' => 'AphrontView',
'HarbormasterUnitStatus' => 'Phobject',
'HarbormasterUnitSummaryView' => 'AphrontView',
'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWorker' => 'PhabricatorWorker',
'HarbormasterWorkingCopyArtifact' => 'HarbormasterDrydockLeaseArtifact',
'HeraldActingUserField' => 'HeraldField',
'HeraldAction' => 'Phobject',
'HeraldActionGroup' => 'HeraldGroup',
'HeraldActionRecord' => 'HeraldDAO',
'HeraldAdapter' => 'Phobject',
+ 'HeraldAdapterDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldAlwaysField' => 'HeraldField',
'HeraldAnotherRuleField' => 'HeraldField',
'HeraldApplicationActionGroup' => 'HeraldActionGroup',
'HeraldApplyTranscript' => 'Phobject',
'HeraldBasicFieldGroup' => 'HeraldFieldGroup',
'HeraldBuildableState' => 'HeraldState',
'HeraldCallWebhookAction' => 'HeraldAction',
'HeraldCommentAction' => 'HeraldAction',
'HeraldCommitAdapter' => array(
'HeraldAdapter',
'HarbormasterBuildableAdapterInterface',
),
'HeraldCondition' => 'HeraldDAO',
'HeraldConditionTranscript' => 'Phobject',
'HeraldContentSourceField' => 'HeraldField',
'HeraldController' => 'PhabricatorController',
'HeraldCoreStateReasons' => 'HeraldStateReasons',
'HeraldCreateWebhooksCapability' => 'PhabricatorPolicyCapability',
'HeraldDAO' => 'PhabricatorLiskDAO',
'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup',
'HeraldDifferentialAdapter' => 'HeraldAdapter',
'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter',
'HeraldDifferentialRevisionAdapter' => array(
'HeraldDifferentialAdapter',
'HarbormasterBuildableAdapterInterface',
),
'HeraldDisableController' => 'HeraldController',
'HeraldDoNothingAction' => 'HeraldAction',
'HeraldEditFieldGroup' => 'HeraldFieldGroup',
'HeraldEffect' => 'Phobject',
'HeraldEmptyFieldValue' => 'HeraldFieldValue',
'HeraldEngine' => 'Phobject',
'HeraldExactProjectsField' => 'HeraldField',
'HeraldField' => 'Phobject',
'HeraldFieldGroup' => 'HeraldGroup',
'HeraldFieldTestCase' => 'PhutilTestCase',
'HeraldFieldValue' => 'Phobject',
'HeraldGroup' => 'Phobject',
'HeraldInvalidActionException' => 'Exception',
'HeraldInvalidConditionException' => 'Exception',
'HeraldMailableState' => 'HeraldState',
'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability',
'HeraldManiphestTaskAdapter' => 'HeraldAdapter',
'HeraldNewController' => 'HeraldController',
'HeraldNewObjectField' => 'HeraldField',
'HeraldNotifyActionGroup' => 'HeraldActionGroup',
'HeraldObjectTranscript' => 'Phobject',
'HeraldPhameBlogAdapter' => 'HeraldAdapter',
'HeraldPhamePostAdapter' => 'HeraldAdapter',
'HeraldPholioMockAdapter' => 'HeraldAdapter',
'HeraldPonderQuestionAdapter' => 'HeraldAdapter',
'HeraldPreCommitAdapter' => 'HeraldAdapter',
'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter',
'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter',
'HeraldPreventActionGroup' => 'HeraldActionGroup',
'HeraldProjectsField' => 'HeraldField',
'HeraldRecursiveConditionsException' => 'Exception',
'HeraldRelatedFieldGroup' => 'HeraldFieldGroup',
'HeraldRemarkupFieldValue' => 'HeraldFieldValue',
'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'HeraldRule' => array(
'HeraldDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
),
+ 'HeraldRuleAdapter' => 'HeraldAdapter',
+ 'HeraldRuleAdapterField' => 'HeraldRuleField',
'HeraldRuleController' => 'HeraldController',
'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'HeraldRuleField' => 'HeraldField',
+ 'HeraldRuleFieldGroup' => 'HeraldFieldGroup',
'HeraldRuleListController' => 'HeraldController',
'HeraldRulePHIDType' => 'PhabricatorPHIDType',
'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldRuleSerializer' => 'Phobject',
'HeraldRuleTestCase' => 'PhabricatorTestCase',
'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction',
'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
'HeraldRuleTranscript' => 'Phobject',
'HeraldRuleTypeConfig' => 'Phobject',
+ 'HeraldRuleTypeDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'HeraldRuleTypeField' => 'HeraldRuleField',
'HeraldRuleViewController' => 'HeraldController',
'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HeraldSelectFieldValue' => 'HeraldFieldValue',
'HeraldSpaceField' => 'HeraldField',
'HeraldState' => 'Phobject',
'HeraldStateReasons' => 'Phobject',
'HeraldSubscribersField' => 'HeraldField',
'HeraldSupportActionGroup' => 'HeraldActionGroup',
'HeraldSupportFieldGroup' => 'HeraldFieldGroup',
'HeraldTestConsoleController' => 'HeraldController',
'HeraldTextFieldValue' => 'HeraldFieldValue',
'HeraldTokenizerFieldValue' => 'HeraldFieldValue',
'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldTranscript' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HeraldTranscriptController' => 'HeraldController',
'HeraldTranscriptDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldTranscriptListController' => 'HeraldController',
'HeraldTranscriptPHIDType' => 'PhabricatorPHIDType',
'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
'HeraldUtilityActionGroup' => 'HeraldActionGroup',
'HeraldWebhook' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
),
'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow',
'HeraldWebhookController' => 'HeraldController',
'HeraldWebhookDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldWebhookEditController' => 'HeraldWebhookController',
'HeraldWebhookEditEngine' => 'PhabricatorEditEngine',
'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldWebhookKeyController' => 'HeraldWebhookController',
'HeraldWebhookListController' => 'HeraldWebhookController',
'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldWebhookRequest' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'HeraldWebhookRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldWebhookRequestListView' => 'AphrontView',
'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldWebhookSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldWebhookStatusTransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookTestController' => 'HeraldWebhookController',
'HeraldWebhookTransaction' => 'PhabricatorModularTransaction',
'HeraldWebhookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldWebhookTransactionType' => 'PhabricatorModularTransactionType',
'HeraldWebhookURITransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookViewController' => 'HeraldWebhookController',
'HeraldWebhookWorker' => 'PhabricatorWorker',
'Javelin' => 'Phobject',
'LegalpadController' => 'PhabricatorController',
'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
'LegalpadDAO' => 'PhabricatorLiskDAO',
'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability',
'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability',
'LegalpadDocument' => array(
'LegalpadDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'LegalpadDocumentBody' => array(
'LegalpadDAO',
'PhabricatorMarkupInterface',
),
'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
'LegalpadDocumentDoneController' => 'LegalpadController',
'LegalpadDocumentEditController' => 'LegalpadController',
'LegalpadDocumentEditEngine' => 'PhabricatorEditEngine',
'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor',
'LegalpadDocumentListController' => 'LegalpadController',
'LegalpadDocumentManageController' => 'LegalpadController',
'LegalpadDocumentPreambleTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'LegalpadDocumentRequireSignatureTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'LegalpadDocumentSignController' => 'LegalpadController',
'LegalpadDocumentSignature' => array(
'LegalpadDAO',
'PhabricatorPolicyInterface',
),
'LegalpadDocumentSignatureAddController' => 'LegalpadController',
'LegalpadDocumentSignatureListController' => 'LegalpadController',
'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine',
'LegalpadDocumentSignatureTypeTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentSignatureVerificationController' => 'LegalpadController',
'LegalpadDocumentSignatureViewController' => 'LegalpadController',
'LegalpadDocumentTextTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentTitleTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentTransactionType' => 'PhabricatorModularTransactionType',
'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver',
'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType',
'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'LegalpadRequireSignatureHeraldAction' => 'HeraldAction',
'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType',
'LegalpadTransaction' => 'PhabricatorModularTransaction',
'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment',
'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView',
'LiskChunkTestCase' => 'PhabricatorTestCase',
'LiskDAO' => 'Phobject',
'LiskDAOSet' => 'Phobject',
'LiskDAOTestCase' => 'PhabricatorTestCase',
'LiskEphemeralObjectException' => 'Exception',
'LiskFixtureTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestDAO' => 'LiskDAO',
'LiskIsolationTestDAOException' => 'Exception',
'LiskMigrationIterator' => 'PhutilBufferedIterator',
'LiskRawMigrationIterator' => 'PhutilBufferedIterator',
'MacroConduitAPIMethod' => 'ConduitAPIMethod',
'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod',
'MacroEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'MacroEmojiExample' => 'PhabricatorUIExample',
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
'ManiphestBulkEditController' => 'ManiphestController',
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
'ManiphestConfiguredCustomField' => array(
'ManiphestCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'ManiphestConstants' => 'Phobject',
'ManiphestController' => 'PhabricatorController',
'ManiphestCreateMailReceiver' => 'PhabricatorMailReceiver',
'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestCustomField' => 'PhabricatorCustomField',
'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser',
'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase',
'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'ManiphestDAO' => 'PhabricatorLiskDAO',
'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability',
'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability',
- 'ManiphestEditAssignCapability' => 'PhabricatorPolicyCapability',
'ManiphestEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ManiphestEditEngine' => 'PhabricatorEditEngine',
- 'ManiphestEditPoliciesCapability' => 'PhabricatorPolicyCapability',
- 'ManiphestEditPriorityCapability' => 'PhabricatorPolicyCapability',
- 'ManiphestEditProjectsCapability' => 'PhabricatorPolicyCapability',
- 'ManiphestEditStatusCapability' => 'PhabricatorPolicyCapability',
'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestMailEngineExtension' => 'PhabricatorMailEngineExtension',
'ManiphestNameIndex' => 'ManiphestDAO',
'ManiphestPointsConfigType' => 'PhabricatorJSONConfigType',
'ManiphestPrioritiesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand',
'ManiphestPrioritySearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestProjectNameFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ManiphestReportController' => 'ManiphestController',
'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ManiphestSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand',
'ManiphestStatusSearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestStatusesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestSubpriorityController' => 'ManiphestController',
'ManiphestSubtypesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestTask' => array(
'ManiphestDAO',
'PhabricatorSubscribableInterface',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMentionableInterface',
'PhrequentTrackableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'DoorkeeperBridgedObjectInterface',
'PhabricatorEditEngineSubtypeInterface',
'PhabricatorEditEngineLockableInterface',
),
'ManiphestTaskAssignHeraldAction' => 'HeraldAction',
'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction',
'ManiphestTaskAssignSelfHeraldAction' => 'ManiphestTaskAssignHeraldAction',
'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine',
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDescriptionHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskDetailController' => 'ManiphestController',
'ManiphestTaskEdgeTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskEditController' => 'ManiphestController',
'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock',
'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine',
'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine',
'ManiphestTaskGraph' => 'PhabricatorObjectGraph',
'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasRevisionRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHeraldField' => 'HeraldField',
'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup',
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskListController' => 'ManiphestController',
'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType',
'ManiphestTaskListView' => 'ManiphestView',
'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskMergedFromTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskMergedIntoTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskOwnerTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver',
'ManiphestTaskPHIDType' => 'PhabricatorPHIDType',
'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPoints' => 'Phobject',
'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPriority' => 'ManiphestConstants',
'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskPriorityHeraldAction' => 'HeraldAction',
'ManiphestTaskPriorityHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskPriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ManiphestTaskRelationship' => 'PhabricatorObjectRelationship',
'ManiphestTaskRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'ManiphestTaskResultListView' => 'ManiphestView',
'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ManiphestTaskStatus' => 'ManiphestConstants',
'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'ManiphestTaskStatusHeraldAction' => 'HeraldAction',
'ManiphestTaskStatusHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase',
'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskTestCase' => 'PhabricatorTestCase',
'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType',
'ManiphestTaskUnblockTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTransaction' => 'PhabricatorModularTransaction',
'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestView' => 'AphrontView',
'MetaMTAEmailTransactionCommand' => 'Phobject',
'MetaMTAEmailTransactionCommandTestCase' => 'PhabricatorTestCase',
'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector',
'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector',
'MetaMTAReceivedMailStatus' => 'Phobject',
'MultimeterContext' => 'MultimeterDimension',
'MultimeterControl' => 'Phobject',
'MultimeterController' => 'PhabricatorController',
'MultimeterDAO' => 'PhabricatorLiskDAO',
'MultimeterDimension' => 'MultimeterDAO',
'MultimeterEvent' => 'MultimeterDAO',
'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector',
'MultimeterHost' => 'MultimeterDimension',
'MultimeterLabel' => 'MultimeterDimension',
'MultimeterSampleController' => 'MultimeterController',
'MultimeterViewer' => 'MultimeterDimension',
'NuanceCommandImplementation' => 'Phobject',
'NuanceConduitAPIMethod' => 'ConduitAPIMethod',
'NuanceConsoleController' => 'NuanceController',
'NuanceContentSource' => 'PhabricatorContentSource',
'NuanceController' => 'PhabricatorController',
'NuanceDAO' => 'PhabricatorLiskDAO',
'NuanceFormItemType' => 'NuanceItemType',
'NuanceGitHubEventItemType' => 'NuanceItemType',
'NuanceGitHubImportCursor' => 'NuanceImportCursor',
'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor',
'NuanceGitHubRawEvent' => 'Phobject',
'NuanceGitHubRawEventTestCase' => 'PhabricatorTestCase',
'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor',
'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition',
'NuanceImportCursor' => 'Phobject',
'NuanceImportCursorData' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
),
'NuanceImportCursorDataQuery' => 'NuanceQuery',
'NuanceImportCursorPHIDType' => 'PhabricatorPHIDType',
'NuanceItem' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'NuanceItemActionController' => 'NuanceController',
'NuanceItemCommand' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
),
'NuanceItemCommandQuery' => 'NuanceQuery',
'NuanceItemCommandSpec' => 'Phobject',
'NuanceItemCommandTransaction' => 'NuanceItemTransactionType',
'NuanceItemController' => 'NuanceController',
'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceItemListController' => 'NuanceItemController',
'NuanceItemManageController' => 'NuanceController',
'NuanceItemOwnerTransaction' => 'NuanceItemTransactionType',
'NuanceItemPHIDType' => 'PhabricatorPHIDType',
'NuanceItemPropertyTransaction' => 'NuanceItemTransactionType',
'NuanceItemQuery' => 'NuanceQuery',
'NuanceItemQueueTransaction' => 'NuanceItemTransactionType',
'NuanceItemRequestorTransaction' => 'NuanceItemTransactionType',
'NuanceItemSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceItemSourceTransaction' => 'NuanceItemTransactionType',
'NuanceItemStatusTransaction' => 'NuanceItemTransactionType',
'NuanceItemTransaction' => 'NuanceTransaction',
'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceItemTransactionType' => 'PhabricatorModularTransactionType',
'NuanceItemType' => 'Phobject',
'NuanceItemUpdateWorker' => 'NuanceWorker',
'NuanceItemViewController' => 'NuanceController',
'NuanceManagementImportWorkflow' => 'NuanceManagementWorkflow',
'NuanceManagementUpdateWorkflow' => 'NuanceManagementWorkflow',
'NuanceManagementWorkflow' => 'PhabricatorManagementWorkflow',
'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition',
'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'NuanceQueue' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'NuanceQueueController' => 'NuanceController',
'NuanceQueueDatasource' => 'PhabricatorTypeaheadDatasource',
'NuanceQueueEditController' => 'NuanceQueueController',
'NuanceQueueEditEngine' => 'PhabricatorEditEngine',
'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceQueueListController' => 'NuanceQueueController',
'NuanceQueueNameTransaction' => 'NuanceQueueTransactionType',
'NuanceQueuePHIDType' => 'PhabricatorPHIDType',
'NuanceQueueQuery' => 'NuanceQuery',
'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceQueueTransaction' => 'NuanceTransaction',
'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceQueueTransactionType' => 'PhabricatorModularTransactionType',
'NuanceQueueViewController' => 'NuanceQueueController',
'NuanceQueueWorkController' => 'NuanceQueueController',
'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'NuanceSource' => array(
'NuanceDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorNgramsInterface',
),
'NuanceSourceActionController' => 'NuanceController',
'NuanceSourceController' => 'NuanceController',
'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceDefaultQueueTransaction' => 'NuanceSourceTransactionType',
'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceDefinition' => 'Phobject',
'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase',
'NuanceSourceEditController' => 'NuanceSourceController',
'NuanceSourceEditEngine' => 'PhabricatorEditEngine',
'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceSourceListController' => 'NuanceSourceController',
'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceNameNgrams' => 'PhabricatorSearchNgrams',
'NuanceSourceNameTransaction' => 'NuanceSourceTransactionType',
'NuanceSourcePHIDType' => 'PhabricatorPHIDType',
'NuanceSourceQuery' => 'NuanceQuery',
'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceSourceTransaction' => 'NuanceTransaction',
'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceSourceTransactionType' => 'PhabricatorModularTransactionType',
'NuanceSourceViewController' => 'NuanceSourceController',
'NuanceTransaction' => 'PhabricatorModularTransaction',
'NuanceTrashCommand' => 'NuanceCommandImplementation',
'NuanceWorker' => 'PhabricatorWorker',
'OwnersConduitAPIMethod' => 'ConduitAPIMethod',
'OwnersEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod',
'OwnersSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PHIDConduitAPIMethod' => 'ConduitAPIMethod',
'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHUI' => 'Phobject',
'PHUIActionPanelExample' => 'PhabricatorUIExample',
'PHUIActionPanelView' => 'AphrontTagView',
'PHUIApplicationMenuView' => 'Phobject',
'PHUIBadgeBoxView' => 'AphrontTagView',
'PHUIBadgeExample' => 'PhabricatorUIExample',
'PHUIBadgeMiniView' => 'AphrontTagView',
'PHUIBadgeView' => 'AphrontTagView',
'PHUIBigInfoExample' => 'PhabricatorUIExample',
'PHUIBigInfoView' => 'AphrontTagView',
'PHUIBoxExample' => 'PhabricatorUIExample',
'PHUIBoxView' => 'AphrontTagView',
'PHUIButtonBarExample' => 'PhabricatorUIExample',
'PHUIButtonBarView' => 'AphrontTagView',
'PHUIButtonExample' => 'PhabricatorUIExample',
'PHUIButtonView' => 'AphrontTagView',
'PHUICMSView' => 'AphrontTagView',
'PHUICalendarDayView' => 'AphrontView',
'PHUICalendarListView' => 'AphrontTagView',
'PHUICalendarMonthView' => 'AphrontView',
'PHUICalendarWeekView' => 'AphrontView',
'PHUICalendarWidgetView' => 'AphrontTagView',
'PHUIColorPalletteExample' => 'PhabricatorUIExample',
'PHUICrumbView' => 'AphrontView',
'PHUICrumbsView' => 'AphrontView',
'PHUICurtainExtension' => 'Phobject',
'PHUICurtainPanelView' => 'AphrontTagView',
'PHUICurtainView' => 'AphrontTagView',
'PHUIDiffGraphView' => 'Phobject',
'PHUIDiffGraphViewTestCase' => 'PhabricatorTestCase',
'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentPreviewListView' => 'AphrontView',
'PHUIDiffInlineCommentRowScaffold' => 'AphrontView',
'PHUIDiffInlineCommentTableScaffold' => 'AphrontView',
'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentView' => 'AphrontView',
'PHUIDiffInlineThreader' => 'Phobject',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDiffRevealIconView' => 'AphrontView',
'PHUIDiffTableOfContentsItemView' => 'AphrontView',
'PHUIDiffTableOfContentsListView' => 'AphrontView',
'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDocumentSummaryView' => 'AphrontTagView',
- 'PHUIDocumentViewPro' => 'AphrontTagView',
+ 'PHUIDocumentView' => 'AphrontTagView',
'PHUIFeedStoryExample' => 'PhabricatorUIExample',
'PHUIFeedStoryView' => 'AphrontView',
'PHUIFormDividerControl' => 'AphrontFormControl',
'PHUIFormFileControl' => 'AphrontFormControl',
'PHUIFormFreeformDateControl' => 'AphrontFormControl',
'PHUIFormIconSetControl' => 'AphrontFormControl',
'PHUIFormInsetView' => 'AphrontView',
'PHUIFormLayoutView' => 'AphrontView',
'PHUIFormNumberControl' => 'AphrontFormControl',
'PHUIHandleListView' => 'AphrontTagView',
'PHUIHandleTagListView' => 'AphrontTagView',
'PHUIHandleView' => 'AphrontView',
'PHUIHeadThingView' => 'AphrontTagView',
'PHUIHeaderView' => 'AphrontTagView',
'PHUIHomeView' => 'AphrontTagView',
'PHUIHovercardUIExample' => 'PhabricatorUIExample',
'PHUIHovercardView' => 'AphrontTagView',
'PHUIIconCircleView' => 'AphrontTagView',
'PHUIIconExample' => 'PhabricatorUIExample',
'PHUIIconView' => 'AphrontTagView',
'PHUIImageMaskExample' => 'PhabricatorUIExample',
'PHUIImageMaskView' => 'AphrontTagView',
'PHUIInfoExample' => 'PhabricatorUIExample',
'PHUIInfoView' => 'AphrontTagView',
'PHUIInvisibleCharacterTestCase' => 'PhabricatorTestCase',
'PHUIInvisibleCharacterView' => 'AphrontView',
'PHUILeftRightExample' => 'PhabricatorUIExample',
'PHUILeftRightView' => 'AphrontTagView',
'PHUIListExample' => 'PhabricatorUIExample',
'PHUIListItemView' => 'AphrontTagView',
'PHUIListView' => 'AphrontTagView',
'PHUIListViewTestCase' => 'PhabricatorTestCase',
'PHUIObjectBoxView' => 'AphrontTagView',
'PHUIObjectItemListExample' => 'PhabricatorUIExample',
'PHUIObjectItemListView' => 'AphrontTagView',
'PHUIObjectItemView' => 'AphrontTagView',
'PHUIPagerView' => 'AphrontView',
'PHUIPinboardItemView' => 'AphrontView',
'PHUIPinboardView' => 'AphrontView',
'PHUIPolicySectionView' => 'AphrontTagView',
'PHUIPropertyGroupView' => 'AphrontTagView',
'PHUIPropertyListExample' => 'PhabricatorUIExample',
'PHUIPropertyListView' => 'AphrontView',
'PHUIRemarkupImageView' => 'AphrontView',
'PHUIRemarkupPreviewPanel' => 'AphrontTagView',
'PHUIRemarkupView' => 'AphrontView',
'PHUISegmentBarSegmentView' => 'AphrontTagView',
'PHUISegmentBarView' => 'AphrontTagView',
'PHUISpacesNamespaceContextView' => 'AphrontView',
'PHUIStatusItemView' => 'AphrontTagView',
'PHUIStatusListView' => 'AphrontTagView',
'PHUITabGroupView' => 'AphrontTagView',
'PHUITabView' => 'AphrontTagView',
'PHUITagExample' => 'PhabricatorUIExample',
'PHUITagView' => 'AphrontTagView',
'PHUITimelineEventView' => 'AphrontView',
'PHUITimelineExample' => 'PhabricatorUIExample',
'PHUITimelineView' => 'AphrontView',
'PHUITwoColumnView' => 'AphrontTagView',
'PHUITypeaheadExample' => 'PhabricatorUIExample',
'PHUIUserAvailabilityView' => 'AphrontTagView',
'PHUIWorkboardView' => 'AphrontTagView',
'PHUIWorkpanelView' => 'AphrontTagView',
'PHUIXComponentsExample' => 'PhabricatorUIExample',
'PassphraseAbstractKey' => 'Phobject',
'PassphraseConduitAPIMethod' => 'ConduitAPIMethod',
'PassphraseController' => 'PhabricatorController',
'PassphraseCredential' => array(
'PassphraseDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PassphraseCredentialAuthorPolicyRule' => 'PhabricatorPolicyRule',
'PassphraseCredentialConduitController' => 'PassphraseController',
'PassphraseCredentialConduitTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialControl' => 'AphrontFormControl',
'PassphraseCredentialCreateController' => 'PassphraseController',
'PassphraseCredentialDescriptionTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialDestroyController' => 'PassphraseController',
'PassphraseCredentialDestroyTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialEditController' => 'PassphraseController',
'PassphraseCredentialFerretEngine' => 'PhabricatorFerretEngine',
'PassphraseCredentialFulltextEngine' => 'PhabricatorFulltextEngine',
'PassphraseCredentialListController' => 'PassphraseController',
'PassphraseCredentialLockController' => 'PassphraseController',
'PassphraseCredentialLockTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialLookedAtTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialNameTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType',
'PassphraseCredentialPublicController' => 'PassphraseController',
'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PassphraseCredentialRevealController' => 'PassphraseController',
'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PassphraseCredentialSecretIDTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialTransaction' => 'PhabricatorModularTransaction',
'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PassphraseCredentialTransactionType' => 'PhabricatorModularTransactionType',
'PassphraseCredentialType' => 'Phobject',
'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase',
'PassphraseCredentialUsernameTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialViewController' => 'PassphraseController',
'PassphraseDAO' => 'PhabricatorLiskDAO',
'PassphraseDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PassphraseDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PassphraseNoteCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordKey' => 'PassphraseAbstractKey',
'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',
'PassphraseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PassphraseSSHGeneratedKeyCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSSHKey' => 'PassphraseAbstractKey',
'PassphraseSSHPrivateKeyCredentialType' => 'PassphraseCredentialType',
'PassphraseSSHPrivateKeyFileCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSSHPrivateKeyTextCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PassphraseSecret' => 'PassphraseDAO',
'PassphraseTokenCredentialType' => 'PassphraseCredentialType',
'PasteConduitAPIMethod' => 'ConduitAPIMethod',
'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteCreateMailReceiver' => 'PhabricatorMailReceiver',
'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PasteEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PasteEmbedView' => 'AphrontView',
'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteLanguageSelectDatasource' => 'PhabricatorTypeaheadDatasource',
'PasteMailReceiver' => 'PhabricatorObjectMailReceiver',
'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PasteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability',
'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability',
+ 'PeopleDisableUsersCapability' => 'PhabricatorPolicyCapability',
'PeopleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PeopleMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector',
'Phabricator404Controller' => 'PhabricatorController',
'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase',
'PhabricatorAccessLog' => 'Phobject',
'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting',
'PhabricatorAccountSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorActionListView' => 'AphrontTagView',
'PhabricatorActionView' => 'AphrontView',
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAnchorView' => 'AphrontView',
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample',
'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase',
'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorApplication' => array(
'PhabricatorLiskDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorApplicationApplicationTransaction' => 'PhabricatorModularTransaction',
'PhabricatorApplicationApplicationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorApplicationConfigOptions' => 'Phobject',
'PhabricatorApplicationConfigurationPanel' => 'Phobject',
'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationEditEngine' => 'PhabricatorEditEngine',
'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView',
'PhabricatorApplicationEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationObjectMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationPolicyChangeTransaction' => 'PhabricatorApplicationTransactionType',
'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorApplicationSearchEngine' => 'Phobject',
'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationSearchResultView' => 'Phobject',
'PhabricatorApplicationTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationTransaction' => array(
'PhabricatorLiskDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorApplicationTransactionComment' => array(
'PhabricatorLiskDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorApplicationTransactionCommentEditController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionCommentHistoryController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionCommentQuoteController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentRawController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentRemoveController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentView' => 'AphrontView',
'PhabricatorApplicationTransactionController' => 'PhabricatorController',
'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
'PhabricatorApplicationTransactionNoEffectException' => 'Exception',
'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker',
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionRemarkupPreviewController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionStructureException' => 'Exception',
'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorApplicationTransactionValidationError' => 'Phobject',
'PhabricatorApplicationTransactionValidationException' => 'Exception',
'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionView' => 'AphrontView',
'PhabricatorApplicationTransactionWarningException' => 'Exception',
'PhabricatorApplicationTransactionWarningResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationUninstallTransaction' => 'PhabricatorApplicationTransactionType',
'PhabricatorApplicationsApplication' => 'PhabricatorApplication',
'PhabricatorApplicationsController' => 'PhabricatorController',
'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController',
'PhabricatorApplyEditField' => 'PhabricatorEditField',
'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAudioDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorAuditActionConstants' => 'Phobject',
'PhabricatorAuditApplication' => 'PhabricatorApplication',
'PhabricatorAuditCommentEditor' => 'PhabricatorEditor',
- 'PhabricatorAuditCommitStatusConstants' => 'Phobject',
'PhabricatorAuditController' => 'PhabricatorController',
'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuditInlineComment' => array(
'Phobject',
'PhabricatorInlineCommentInterface',
),
'PhabricatorAuditListView' => 'AphrontView',
'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuditStatusConstants' => 'Phobject',
'PhabricatorAuditSynchronizeManagementWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuditTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView',
'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuthAccountView' => 'AphrontView',
'PhabricatorAuthApplication' => 'PhabricatorApplication',
'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthFactor' => 'Phobject',
'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
'PhabricatorAuthHighSecurityToken' => 'Phobject',
'PhabricatorAuthInvite' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteAction' => 'Phobject',
'PhabricatorAuthInviteActionTableView' => 'AphrontView',
'PhabricatorAuthInviteController' => 'PhabricatorAuthController',
'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException',
'PhabricatorAuthInviteEngine' => 'Phobject',
'PhabricatorAuthInviteException' => 'Exception',
'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException',
'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteWorker' => 'PhabricatorWorker',
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthLoginHandler' => 'Phobject',
'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRevokeWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementUnlimitWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController',
'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPassword' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthPasswordEngine' => 'Phobject',
'PhabricatorAuthPasswordException' => 'Exception',
'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthPasswordTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthPasswordTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthPasswordUpgradeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthProvider' => 'Phobject',
'PhabricatorAuthProviderConfig' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController',
'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController',
'PhabricatorAuthRevoker' => 'Phobject',
'PhabricatorAuthSSHKey' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyListController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuthSSHKeyRevokeController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
'PhabricatorAuthSSHKeyTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHPublicKey' => 'Phobject',
'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthSession' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthSessionEngine' => 'Phobject',
'PhabricatorAuthSessionEngineExtension' => 'Phobject',
'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthSessionInfo' => 'Phobject',
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthTemporaryToken' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthTemporaryTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthTemporaryTokenType' => 'Phobject',
'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule',
'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAutoEventListener' => 'PhabricatorEventListener',
'PhabricatorBadgesApplication' => 'PhabricatorApplication',
'PhabricatorBadgesArchiveController' => 'PhabricatorBadgesController',
'PhabricatorBadgesAward' => array(
'PhabricatorBadgesDAO',
'PhabricatorDestructibleInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorBadgesAwardController' => 'PhabricatorBadgesController',
'PhabricatorBadgesAwardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorBadgesAwardTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorBadgesBadge' => array(
'PhabricatorBadgesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorBadgesBadgeAwardTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeDescriptionTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeFlavorTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeIconTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorBadgesBadgeNameTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeQualityTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeRevokeTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeStatusTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorBadgesBadgeTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorBadgesCommentController' => 'PhabricatorBadgesController',
'PhabricatorBadgesController' => 'PhabricatorController',
'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability',
'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO',
'PhabricatorBadgesDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorBadgesEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorBadgesEditController' => 'PhabricatorBadgesController',
'PhabricatorBadgesEditEngine' => 'PhabricatorEditEngine',
'PhabricatorBadgesEditRecipientsController' => 'PhabricatorBadgesController',
'PhabricatorBadgesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorBadgesIconSet' => 'PhabricatorIconSet',
'PhabricatorBadgesListController' => 'PhabricatorBadgesController',
'PhabricatorBadgesLootContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorBadgesMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorBadgesPHIDType' => 'PhabricatorPHIDType',
'PhabricatorBadgesProfileController' => 'PhabricatorController',
'PhabricatorBadgesQuality' => 'Phobject',
'PhabricatorBadgesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorBadgesRecipientsController' => 'PhabricatorBadgesProfileController',
'PhabricatorBadgesRecipientsListView' => 'AphrontView',
'PhabricatorBadgesRemoveRecipientsController' => 'PhabricatorBadgesController',
'PhabricatorBadgesReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorBadgesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorBadgesSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorBadgesSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorBadgesTransaction' => 'PhabricatorModularTransaction',
'PhabricatorBadgesTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorBadgesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorBadgesViewController' => 'PhabricatorBadgesProfileController',
'PhabricatorBarePageView' => 'AphrontPageView',
'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorBoardColumnsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorBoardLayoutEngine' => 'Phobject',
'PhabricatorBoardRenderingEngine' => 'Phobject',
'PhabricatorBoardResponseEngine' => 'Phobject',
'PhabricatorBoolConfigType' => 'PhabricatorTextConfigType',
'PhabricatorBoolEditField' => 'PhabricatorEditField',
'PhabricatorBoolMailStamp' => 'PhabricatorMailStamp',
'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine',
'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
'PhabricatorBulkEditGroup' => 'Phobject',
'PhabricatorBulkEngine' => 'Phobject',
'PhabricatorBulkManagementExportWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
'PhabricatorCacheEngine' => 'Phobject',
'PhabricatorCacheEngineExtension' => 'Phobject',
'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow',
'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCachePurger' => 'Phobject',
'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorCacheSpec' => 'Phobject',
'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCachedClassMapQuery' => 'Phobject',
'PhabricatorCaches' => 'Phobject',
'PhabricatorCachesTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarApplication' => 'PhabricatorApplication',
'PhabricatorCalendarController' => 'PhabricatorController',
'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO',
'PhabricatorCalendarEvent' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorPolicyCodexInterface',
'PhabricatorProjectInterface',
'PhabricatorMarkupInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDeclineTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCalendarEventDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCalendarEventDescriptionTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter',
'PhabricatorCalendarEventHeraldField' => 'HeraldField',
'PhabricatorCalendarEventHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorCalendarEventHostPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorCalendarEventHostTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventIconTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventInviteTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventInvitee' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventInviteesPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField',
'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventNotificationView' => 'Phobject',
'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarEventPolicyCodex' => 'PhabricatorPolicyCodex',
'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
'PhabricatorCalendarEventRecurringTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventReplyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarEventStartDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarEventTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarEventUntilDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarExportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportDisableTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExternalInvitee' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine',
'PhabricatorCalendarICSURIImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSWriter' => 'Phobject',
'PhabricatorCalendarIconSet' => 'PhabricatorIconSet',
'PhabricatorCalendarImport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDeleteLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDropController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportEngine' => 'Phobject',
'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSWarningLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLog' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarImportLogListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarImportLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarImportLogType' => 'Phobject',
'PhabricatorCalendarImportLogView' => 'AphrontView',
'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportOriginalLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarImportQueueLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportReloadController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportReloadTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportReloadWorker' => 'PhabricatorWorker',
'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarInviteeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementReloadWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCalendarNotification' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarNotificationEngine' => 'Phobject',
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCelerityApplication' => 'PhabricatorApplication',
'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogApplication' => 'PhabricatorApplication',
'PhabricatorChatLogChannel' => array(
'PhabricatorChatLogDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorChatLogController' => 'PhabricatorController',
'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO',
'PhabricatorChatLogEvent' => array(
'PhabricatorChatLogDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorCheckboxesEditField' => 'PhabricatorEditField',
'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorClassConfigType' => 'PhabricatorTextConfigType',
'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorClusterDatabasesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterException' => 'Exception',
'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterMailersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterNoHostForRoleException' => 'Exception',
'PhabricatorClusterSearchConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterServiceHealthRecord' => 'Phobject',
'PhabricatorClusterStrandedException' => 'PhabricatorClusterException',
'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCommentEditField' => 'PhabricatorEditField',
'PhabricatorCommentEditType' => 'PhabricatorEditType',
'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitCustomField' => 'PhabricatorCustomField',
'PhabricatorCommitMergedCommitsField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitRepositoryField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField',
'PhabricatorCommonPasswords' => 'Phobject',
'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
'PhabricatorConduitApplication' => 'PhabricatorApplication',
'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow',
'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO',
'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
'PhabricatorConduitContentSource' => 'PhabricatorContentSource',
'PhabricatorConduitController' => 'PhabricatorController',
'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
'PhabricatorConduitEditField' => 'PhabricatorEditField',
'PhabricatorConduitListController' => 'PhabricatorConduitController',
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConduitMethodCallLog' => array(
'PhabricatorConduitDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorConduitResultInterface' => 'PhabricatorPHIDInterface',
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitSearchFieldSpecification' => 'Phobject',
'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
'PhabricatorConduitToken' => array(
'PhabricatorConduitDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorConfigAllController' => 'PhabricatorConfigController',
'PhabricatorConfigApplication' => 'PhabricatorApplication',
'PhabricatorConfigApplicationController' => 'PhabricatorConfigController',
'PhabricatorConfigCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController',
'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
'PhabricatorConfigConstants' => 'Phobject',
'PhabricatorConfigController' => 'PhabricatorController',
'PhabricatorConfigCoreSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorConfigDatabaseController' => 'PhabricatorConfigController',
'PhabricatorConfigDatabaseIssueController' => 'PhabricatorConfigDatabaseController',
'PhabricatorConfigDatabaseSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigDatabaseSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDatabaseStatusController' => 'PhabricatorConfigDatabaseController',
'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
'PhabricatorConfigEdgeModule' => 'PhabricatorConfigModule',
'PhabricatorConfigEditController' => 'PhabricatorConfigController',
'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorConfigEntry' => array(
'PhabricatorConfigEntryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigGroupConstants' => 'PhabricatorConfigConstants',
'PhabricatorConfigGroupController' => 'PhabricatorConfigController',
'PhabricatorConfigHTTPParameterTypesModule' => 'PhabricatorConfigModule',
'PhabricatorConfigHistoryController' => 'PhabricatorConfigController',
'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController',
'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
'PhabricatorConfigIssuePanelController' => 'PhabricatorConfigController',
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
'PhabricatorConfigJSON' => 'Phobject',
'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType',
'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigListController' => 'PhabricatorConfigController',
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementDoneWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConfigManualActivity' => 'PhabricatorConfigEntryDAO',
'PhabricatorConfigModule' => 'Phobject',
'PhabricatorConfigModuleController' => 'PhabricatorConfigController',
- 'PhabricatorConfigOption' => array(
- 'Phobject',
- 'PhabricatorMarkupInterface',
- ),
+ 'PhabricatorConfigOption' => 'Phobject',
'PhabricatorConfigOptionType' => 'Phobject',
'PhabricatorConfigPHIDModule' => 'PhabricatorConfigModule',
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigPurgeCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigRegexOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorConfigRequestExceptionHandlerModule' => 'PhabricatorConfigModule',
'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
'PhabricatorConfigSchemaQuery' => 'Phobject',
'PhabricatorConfigSchemaSpec' => 'Phobject',
'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigSetupCheckModule' => 'PhabricatorConfigModule',
'PhabricatorConfigSiteModule' => 'PhabricatorConfigModule',
'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigSource' => 'Phobject',
'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
'PhabricatorConfigStorageSchema' => 'Phobject',
'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorConfigType' => 'Phobject',
'PhabricatorConfigValidationException' => 'Exception',
'PhabricatorConfigVersionController' => 'PhabricatorConfigController',
'PhabricatorConpherenceApplication' => 'PhabricatorApplication',
'PhabricatorConpherenceColumnMinimizeSetting' => 'PhabricatorInternalSetting',
'PhabricatorConpherenceColumnVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConpherenceNotificationsSetting' => 'PhabricatorSelectSetting',
'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorConpherenceProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorConpherenceRoomContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorConpherenceRoomTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorConpherenceSoundSetting' => 'PhabricatorSelectSetting',
'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType',
'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConsoleApplication' => 'PhabricatorApplication',
'PhabricatorConsoleContentSource' => 'PhabricatorContentSource',
'PhabricatorContentSource' => 'Phobject',
'PhabricatorContentSourceModule' => 'PhabricatorConfigModule',
'PhabricatorContentSourceView' => 'AphrontView',
'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorController' => 'AphrontController',
'PhabricatorCookies' => 'Phobject',
'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType',
'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType',
'PhabricatorCountFact' => 'PhabricatorFact',
'PhabricatorCountdown' => array(
'PhabricatorCountdownDAO',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSpacesInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorCountdownApplication' => 'PhabricatorApplication',
'PhabricatorCountdownController' => 'PhabricatorController',
'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO',
'PhabricatorCountdownDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCountdownDescriptionTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownEditController' => 'PhabricatorCountdownController',
'PhabricatorCountdownEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCountdownEpochTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownListController' => 'PhabricatorCountdownController',
'PhabricatorCountdownMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCountdownReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCountdownSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCountdownTitleTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCountdownTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCountdownView' => 'AphrontView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorCustomField' => 'Phobject',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCustomFieldApplicationSearchDatasource' => 'PhabricatorTypeaheadProxyDatasource',
'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCustomFieldAttachment' => 'Phobject',
'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
'PhabricatorCustomFieldEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCustomFieldEditField' => 'PhabricatorEditField',
'PhabricatorCustomFieldEditType' => 'PhabricatorEditType',
'PhabricatorCustomFieldExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorCustomFieldFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorCustomFieldHeraldAction' => 'HeraldAction',
'PhabricatorCustomFieldHeraldActionGroup' => 'HeraldActionGroup',
'PhabricatorCustomFieldHeraldField' => 'HeraldField',
'PhabricatorCustomFieldHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldList' => 'Phobject',
'PhabricatorCustomFieldMonogramParser' => 'Phobject',
'PhabricatorCustomFieldNotAttachedException' => 'Exception',
'PhabricatorCustomFieldNotProxyException' => 'Exception',
'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomFieldSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldStorageQuery' => 'Phobject',
'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomLogoConfigType' => 'PhabricatorConfigOptionType',
'PhabricatorCustomUIFooterConfigType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorDaemon' => 'PhutilDaemon',
'PhabricatorDaemonBulkJobController' => 'PhabricatorDaemonController',
'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
'PhabricatorDaemonContentSource' => 'PhabricatorContentSource',
'PhabricatorDaemonController' => 'PhabricatorController',
'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO',
'PhabricatorDaemonEventListener' => 'PhabricatorEventListener',
'PhabricatorDaemonLockLog' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLockLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLog' => array(
'PhabricatorDaemonDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogEventsView' => 'AphrontView',
'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogListView' => 'AphrontView',
'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementLogWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementReloadWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDaemonOverseerModule' => 'PhutilDaemonOverseerModule',
'PhabricatorDaemonReference' => 'Phobject',
'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonTasksTableView' => 'AphrontView',
'PhabricatorDaemonsApplication' => 'PhabricatorApplication',
'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorDarkConsoleSetting' => 'PhabricatorSelectSetting',
'PhabricatorDarkConsoleTabSetting' => 'PhabricatorInternalSetting',
'PhabricatorDarkConsoleVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorDashboard' => array(
'PhabricatorDashboardDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardApplication' => 'PhabricatorApplication',
'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController',
'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardProfileController',
'PhabricatorDashboardController' => 'PhabricatorController',
'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType',
'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardIconSet' => 'PhabricatorIconSet',
'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO',
'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardLayoutConfig' => 'Phobject',
'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardManageController' => 'PhabricatorDashboardProfileController',
'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorDashboardPanel' => array(
'PhabricatorDashboardDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelCoreCustomField' => array(
'PhabricatorDashboardPanelCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField',
'PhabricatorDashboardPanelDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorDashboardPanelEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelEditEngine' => 'PhabricatorEditEngine',
'PhabricatorDashboardPanelEditproController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType',
'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorDashboardPanelPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardPanelRenderController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelRenderingEngine' => 'Phobject',
'PhabricatorDashboardPanelSearchApplicationCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardPanelSearchQueryCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelTabsCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardPanelType' => 'Phobject',
'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController',
'PhabricatorDashboardProfileController' => 'PhabricatorController',
'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardQueryPanelInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardRenderingEngine' => 'Phobject',
'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController',
'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorDataNotAttachedException' => 'Exception',
'PhabricatorDatabaseRef' => 'Phobject',
'PhabricatorDatabaseRefParser' => 'Phobject',
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDatasourceApplicationEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorDatasourceEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorDatasourceEngine' => 'Phobject',
'PhabricatorDatasourceEngineExtension' => 'Phobject',
'PhabricatorDateFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorDateTimeSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDebugController' => 'PhabricatorController',
'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle',
'PhabricatorDestructibleCodex' => 'Phobject',
'PhabricatorDestructionEngine' => 'Phobject',
'PhabricatorDestructionEngineExtension' => 'Phobject',
'PhabricatorDestructionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDifferenceEngine' => 'Phobject',
'PhabricatorDifferentialApplication' => 'PhabricatorApplication',
'PhabricatorDifferentialAttachCommitWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
+ 'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting',
'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorDividerEditField' => 'PhabricatorEditField',
'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDivinerApplication' => 'PhabricatorApplication',
'PhabricatorDocumentEngine' => 'Phobject',
'PhabricatorDocumentRef' => 'Phobject',
'PhabricatorDocumentRenderingEngine' => 'Phobject',
'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
+ 'PhabricatorDoubleExportField' => 'PhabricatorExportField',
'PhabricatorDraft' => 'PhabricatorDraftDAO',
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
'PhabricatorDraftEngine' => 'Phobject',
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
'PhabricatorEdgeChangeRecord' => 'Phobject',
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
'PhabricatorEdgeConstants' => 'Phobject',
'PhabricatorEdgeCycleException' => 'Exception',
'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorEdgeEditor' => 'Phobject',
'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
'PhabricatorEdgeObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEdgeQuery' => 'PhabricatorQuery',
'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeType' => 'Phobject',
'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorEditEngine' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod',
'PhabricatorEditEngineBulkJobType' => 'PhabricatorWorkerBulkJobType',
'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineCommentAction' => 'Phobject',
'PhabricatorEditEngineCommentActionGroup' => 'Phobject',
'PhabricatorEditEngineConfiguration' => array(
'PhabricatorSearchDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorEditEngineConfigurationDefaultCreateController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationDefaultsController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationDisableController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationEditController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationEditEngine' => 'PhabricatorEditEngine',
'PhabricatorEditEngineConfigurationEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorEditEngineConfigurationIsEditController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationLockController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorEditEngineConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEditEngineConfigurationReorderController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSaveController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorEditEngineConfigurationSortController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSubtypeController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorEditEngineConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController',
'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock',
'PhabricatorEditEngineExtension' => 'Phobject',
'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineLock' => 'Phobject',
'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorEditEngineSelectCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineSubtype' => 'Phobject',
'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase',
'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditField' => 'Phobject',
'PhabricatorEditPage' => 'Phobject',
'PhabricatorEditType' => 'Phobject',
'PhabricatorEditor' => 'Phobject',
'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost',
'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailContentSource' => 'PhabricatorContentSource',
'PhabricatorEmailDeliverySettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorEmailFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
'PhabricatorEmailNotificationsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailSelfActionsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailStampsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailTagsSetting' => 'PhabricatorInternalSetting',
'PhabricatorEmailVarySubjectsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailVerificationController' => 'PhabricatorAuthController',
'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorEmojiDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorEmojiTranslation' => 'PhutilTranslation',
'PhabricatorEmptyQueryException' => 'Exception',
'PhabricatorEnumConfigType' => 'PhabricatorTextConfigType',
'PhabricatorEnv' => 'Phobject',
'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
'PhabricatorEpochEditField' => 'PhabricatorEditField',
'PhabricatorEpochExportField' => 'PhabricatorExportField',
'PhabricatorEvent' => 'PhutilEvent',
'PhabricatorEventEngine' => 'Phobject',
'PhabricatorEventListener' => 'PhutilEventListener',
'PhabricatorEventType' => 'PhutilEventType',
'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat',
'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorExportEngine' => 'Phobject',
'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType',
'PhabricatorExportEngineExtension' => 'Phobject',
'PhabricatorExportField' => 'Phobject',
'PhabricatorExportFormat' => 'Phobject',
'PhabricatorExportFormatSetting' => 'PhabricatorInternalSetting',
'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorExternalAccount' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorFact' => 'Phobject',
'PhabricatorFactAggregate' => 'PhabricatorFactDAO',
'PhabricatorFactApplication' => 'PhabricatorApplication',
'PhabricatorFactChartController' => 'PhabricatorFactController',
'PhabricatorFactController' => 'PhabricatorController',
'PhabricatorFactCursor' => 'PhabricatorFactDAO',
'PhabricatorFactDAO' => 'PhabricatorLiskDAO',
'PhabricatorFactDaemon' => 'PhabricatorDaemon',
'PhabricatorFactDatapointQuery' => 'Phobject',
'PhabricatorFactDimension' => 'PhabricatorFactDAO',
'PhabricatorFactEngine' => 'Phobject',
'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFactHomeController' => 'PhabricatorFactController',
'PhabricatorFactIntDatapoint' => 'PhabricatorFactDAO',
'PhabricatorFactKeyDimension' => 'PhabricatorFactDimension',
'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFactObjectController' => 'PhabricatorFactController',
'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension',
'PhabricatorFactRaw' => 'PhabricatorFactDAO',
'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
'PhabricatorFaviconRef' => 'Phobject',
'PhabricatorFaviconRefQuery' => 'Phobject',
'PhabricatorFavoritesApplication' => 'PhabricatorApplication',
'PhabricatorFavoritesController' => 'PhabricatorController',
'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorFavoritesMenuItemController' => 'PhabricatorFavoritesController',
'PhabricatorFavoritesProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorFaxContentSource' => 'PhabricatorContentSource',
'PhabricatorFeedApplication' => 'PhabricatorApplication',
'PhabricatorFeedBuilder' => 'Phobject',
'PhabricatorFeedConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFeedController' => 'PhabricatorController',
'PhabricatorFeedDAO' => 'PhabricatorLiskDAO',
'PhabricatorFeedDetailController' => 'PhabricatorFeedController',
'PhabricatorFeedListController' => 'PhabricatorFeedController',
'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow',
'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFeedStory' => array(
'Phobject',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
),
'PhabricatorFeedStoryData' => array(
'PhabricatorFeedDAO',
'PhabricatorDestructibleInterface',
),
'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryPublisher' => 'Phobject',
'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
'PhabricatorFerretEngine' => 'Phobject',
'PhabricatorFerretEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFerretFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorFerretFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorFerretMetadata' => 'Phobject',
'PhabricatorFerretSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorFile' => array(
'PhabricatorFileDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorIndexableInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileBundleLoader' => 'Phobject',
'PhabricatorFileChunk' => array(
'PhabricatorFileDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorFileChunkIterator' => array(
'Phobject',
'Iterator',
),
'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileComposeController' => 'PhabricatorFileController',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
'PhabricatorFileDataController' => 'PhabricatorFileController',
'PhabricatorFileDeleteController' => 'PhabricatorFileController',
'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileDocumentController' => 'PhabricatorFileController',
'PhabricatorFileDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine',
'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
'PhabricatorFileEditController' => 'PhabricatorFileController',
'PhabricatorFileEditEngine' => 'PhabricatorEditEngine',
'PhabricatorFileEditField' => 'PhabricatorEditField',
'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorFileExternalRequest' => array(
'PhabricatorFileDAO',
'PhabricatorDestructibleInterface',
),
'PhabricatorFileExternalRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType',
'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorFileIconSetSelectController' => 'PhabricatorFileController',
'PhabricatorFileImageMacro' => array(
'PhabricatorFileDAO',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorFileImageProxyController' => 'PhabricatorFileController',
'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
'PhabricatorFileIntegrityException' => 'Exception',
'PhabricatorFileLightboxController' => 'PhabricatorFileController',
'PhabricatorFileLinkView' => 'AphrontTagView',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorFileSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileStorageConfigurationException' => 'Exception',
'PhabricatorFileStorageEngine' => 'Phobject',
'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFileStorageFormat' => 'Phobject',
'PhabricatorFileStorageFormatTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorFileTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform',
'PhabricatorFileTransaction' => 'PhabricatorModularTransaction',
'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorFileTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorFileTransform' => 'Phobject',
'PhabricatorFileTransformController' => 'PhabricatorFileController',
'PhabricatorFileTransformListController' => 'PhabricatorFileController',
'PhabricatorFileTransformTestCase' => 'PhabricatorTestCase',
'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileUploadDialogController' => 'PhabricatorFileController',
'PhabricatorFileUploadException' => 'Exception',
'PhabricatorFileUploadSource' => 'Phobject',
'PhabricatorFileUploadSourceByteLimitException' => 'Exception',
'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorFilesApplication' => 'PhabricatorApplication',
'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorFilesBuiltinFile' => 'Phobject',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementCycleWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementIntegrityWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFilesOnDiskBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction',
'PhabricatorFiletreeVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorFiletreeWidthSetting' => 'PhabricatorInternalSetting',
'PhabricatorFlag' => array(
'PhabricatorFlagDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction',
'PhabricatorFlagColor' => 'PhabricatorFlagConstants',
'PhabricatorFlagConstants' => 'Phobject',
'PhabricatorFlagController' => 'PhabricatorController',
'PhabricatorFlagDAO' => 'PhabricatorLiskDAO',
'PhabricatorFlagDeleteController' => 'PhabricatorFlagController',
'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorFlagEditController' => 'PhabricatorFlagController',
'PhabricatorFlagListController' => 'PhabricatorFlagController',
'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFlagSelectControl' => 'AphrontFormControl',
'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface',
'PhabricatorFlagsApplication' => 'PhabricatorApplication',
'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorFulltextEngine' => 'Phobject',
'PhabricatorFulltextEngineExtension' => 'Phobject',
'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorFulltextIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorFulltextInterface' => 'PhabricatorIndexableInterface',
'PhabricatorFulltextResultSet' => 'Phobject',
'PhabricatorFulltextStorageEngine' => 'Phobject',
'PhabricatorFulltextToken' => 'Phobject',
'PhabricatorFundApplication' => 'PhabricatorApplication',
'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorGarbageCollector' => 'Phobject',
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorGeneralCachePurger' => 'PhabricatorCachePurger',
'PhabricatorGestureUIExample' => 'PhabricatorUIExample',
'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGlobalLock' => 'PhutilLock',
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGuidanceContext' => 'Phobject',
'PhabricatorGuidanceEngine' => 'Phobject',
'PhabricatorGuidanceEngineExtension' => 'Phobject',
'PhabricatorGuidanceMessage' => 'Phobject',
'PhabricatorGuideApplication' => 'PhabricatorApplication',
'PhabricatorGuideController' => 'PhabricatorController',
'PhabricatorGuideInstallModule' => 'PhabricatorGuideModule',
'PhabricatorGuideItemView' => 'Phobject',
'PhabricatorGuideListView' => 'AphrontView',
'PhabricatorGuideModule' => 'Phobject',
'PhabricatorGuideModuleController' => 'PhabricatorGuideController',
'PhabricatorGuideQuickStartModule' => 'PhabricatorGuideModule',
'PhabricatorHMACTestCase' => 'PhabricatorTestCase',
'PhabricatorHTTPParameterTypeTableView' => 'AphrontView',
'PhabricatorHandleList' => array(
'Phobject',
'Iterator',
'ArrayAccess',
'Countable',
),
'PhabricatorHandleObjectSelectorDataView' => 'Phobject',
'PhabricatorHandlePool' => 'Phobject',
'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase',
'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorHandleRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorHandlesEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
'PhabricatorHash' => 'Phobject',
'PhabricatorHashTestCase' => 'PhabricatorTestCase',
'PhabricatorHelpApplication' => 'PhabricatorApplication',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController',
'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorHeraldApplication' => 'PhabricatorApplication',
'PhabricatorHeraldContentSource' => 'PhabricatorContentSource',
'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorHomeApplication' => 'PhabricatorApplication',
'PhabricatorHomeConstants' => 'PhabricatorHomeController',
'PhabricatorHomeController' => 'PhabricatorController',
'PhabricatorHomeLauncherProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorHomeMenuItemController' => 'PhabricatorHomeController',
'PhabricatorHomeProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorHomeProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorHovercardEngineExtension' => 'Phobject',
'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorIDExportField' => 'PhabricatorExportField',
'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorIconSet' => 'Phobject',
'PhabricatorIconSetEditField' => 'PhabricatorEditField',
'PhabricatorIconSetIcon' => 'Phobject',
'PhabricatorImageDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorImageRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorImageTransformer' => 'Phobject',
'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorInFlightErrorView' => 'AphrontView',
'PhabricatorIndexEngine' => 'Phobject',
'PhabricatorIndexEngineExtension' => 'Phobject',
'PhabricatorIndexEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase',
'PhabricatorInlineCommentController' => 'PhabricatorController',
'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface',
'PhabricatorInlineCommentPreviewController' => 'PhabricatorController',
'PhabricatorInlineSummaryView' => 'AphrontView',
'PhabricatorInstructionsEditField' => 'PhabricatorEditField',
'PhabricatorIntConfigType' => 'PhabricatorTextConfigType',
'PhabricatorIntEditField' => 'PhabricatorEditField',
'PhabricatorIntExportField' => 'PhabricatorExportField',
'PhabricatorInternalSetting' => 'PhabricatorSetting',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType',
'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
'PhabricatorJavelinLinter' => 'ArcanistLinter',
'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorJupyterDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
'PhabricatorKeyValueSerializingCacheProxy' => 'PhutilKeyValueCacheProxy',
'PhabricatorKeyboardRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorKeyring' => 'Phobject',
'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorLegalpadApplication' => 'PhabricatorApplication',
'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType',
'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase',
'PhabricatorLinkProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorLipsumArtist' => 'Phobject',
'PhabricatorLipsumContentSource' => 'PhabricatorContentSource',
'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow',
'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist',
'PhabricatorLiskDAO' => 'LiskDAO',
'PhabricatorLiskExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorLiskFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorLiskSerializer' => 'Phobject',
'PhabricatorListExportField' => 'PhabricatorExportField',
'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase',
'PhabricatorLocaleScopeGuard' => 'Phobject',
'PhabricatorLocaleScopeGuardTestCase' => 'PhabricatorTestCase',
'PhabricatorLockLogManagementWorkflow' => 'PhabricatorLockManagementWorkflow',
'PhabricatorLockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorLogoutController' => 'PhabricatorAuthController',
'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorMacroApplication' => 'PhabricatorApplication',
'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroAudioController' => 'PhabricatorMacroController',
'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMacroController' => 'PhabricatorController',
'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorMacroDisableController' => 'PhabricatorMacroController',
'PhabricatorMacroDisabledTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroEditController' => 'PhameBlogController',
'PhabricatorMacroEditEngine' => 'PhabricatorEditEngine',
'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorMacroFileTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroListController' => 'PhabricatorMacroController',
'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability',
'PhabricatorMacroMemeController' => 'PhabricatorMacroController',
'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController',
'PhabricatorMacroNameTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMacroTestCase' => 'PhabricatorTestCase',
'PhabricatorMacroTransaction' => 'PhabricatorModularTransaction',
'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorMacroViewController' => 'PhabricatorMacroController',
'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase',
'PhabricatorMailEmailHeraldField' => 'HeraldField',
'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorMailEmailSubjectHeraldField' => 'PhabricatorMailEmailHeraldField',
'PhabricatorMailEngineExtension' => 'Phobject',
'PhabricatorMailImplementationAdapter' => 'Phobject',
'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter',
'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMailImplementationPostmarkAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction',
'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter',
'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction',
'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction',
'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction',
'PhabricatorMailOutboundStatus' => 'Phobject',
+ 'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorMailReceiver' => 'Phobject',
'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorMailReplyHandler' => 'Phobject',
'PhabricatorMailRoutingRule' => 'Phobject',
'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMailStamp' => 'Phobject',
'PhabricatorMailTarget' => 'Phobject',
'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMainMenuBarExtension' => 'Phobject',
'PhabricatorMainMenuSearchView' => 'AphrontView',
'PhabricatorMainMenuView' => 'AphrontView',
'PhabricatorManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorManiphestApplication' => 'PhabricatorApplication',
'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorManiphestTaskFactEngine' => 'PhabricatorTransactionFactEngine',
'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorManualActivitySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
'PhabricatorMarkupEngine' => 'Phobject',
'PhabricatorMarkupEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorMarkupOneOff' => array(
'Phobject',
'PhabricatorMarkupInterface',
),
'PhabricatorMarkupPreviewController' => 'PhabricatorController',
'PhabricatorMemeEngine' => 'Phobject',
'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorMetaMTAActor' => 'Phobject',
'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery',
'PhabricatorMetaMTAApplication' => 'PhabricatorApplication',
'PhabricatorMetaMTAApplicationEmail' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
),
'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorMetaMTAApplicationEmailEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorMetaMTAApplicationEmailHeraldField' => 'HeraldField',
'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAApplicationEmailTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorMetaMTAAttachment' => 'Phobject',
'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMetaMTAController' => 'PhabricatorController',
'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
'PhabricatorMetaMTAEmailBodyParser' => 'Phobject',
'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAEmailHeraldAction' => 'HeraldAction',
'PhabricatorMetaMTAEmailOthersHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction',
'PhabricatorMetaMTAEmailSelfHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction',
'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction',
'PhabricatorMetaMTAMail' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorMetaMTAMailBody' => 'Phobject',
'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorMetaMTAMailProperties' => array(
+ 'PhabricatorMetaMTADAO',
+ 'PhabricatorPolicyInterface',
+ ),
+ 'PhabricatorMetaMTAMailPropertiesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMetaMTAMailSection' => 'Phobject',
'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailViewController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery',
'PhabricatorMetaMTAPermanentFailureException' => 'Exception',
'PhabricatorMetaMTAPostmarkReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception',
'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorModularTransactionType' => 'Phobject',
'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorMonospacedFontSetting' => 'PhabricatorStringSetting',
'PhabricatorMonospacedTextareasSetting' => 'PhabricatorSelectSetting',
'PhabricatorMotivatorProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample',
'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorMultimeterApplication' => 'PhabricatorApplication',
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorNamedQueryConfig' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorNamedQueryConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorNgramsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorNgramsInterface' => 'PhabricatorIndexableInterface',
'PhabricatorNotificationBuilder' => 'Phobject',
'PhabricatorNotificationClearController' => 'PhabricatorNotificationController',
'PhabricatorNotificationClient' => 'Phobject',
'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorNotificationController' => 'PhabricatorController',
'PhabricatorNotificationDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController',
'PhabricatorNotificationListController' => 'PhabricatorNotificationController',
'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController',
'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorNotificationServerRef' => 'Phobject',
'PhabricatorNotificationServersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorNotificationStatusView' => 'AphrontTagView',
'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory',
'PhabricatorNotificationUIExample' => 'PhabricatorUIExample',
'PhabricatorNotificationsApplication' => 'PhabricatorApplication',
'PhabricatorNotificationsSetting' => 'PhabricatorInternalSetting',
'PhabricatorNotificationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorNuanceApplication' => 'PhabricatorApplication',
'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider',
'PhabricatorOAuth1SecretTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider',
'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorOAuthClientAuthorization' => array(
'PhabricatorOAuthServerDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthClientDisableController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientTestController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthResponse' => 'AphrontResponse',
'PhabricatorOAuthServer' => 'Phobject',
'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthServerApplication' => 'PhabricatorApplication',
'PhabricatorOAuthServerAuthController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorOAuthServerClient' => array(
'PhabricatorOAuthServerDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType',
'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOAuthServerController' => 'PhabricatorController',
'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO',
'PhabricatorOAuthServerEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOAuthServerEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOAuthServerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOAuthServerScope' => 'Phobject',
'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
'PhabricatorOAuthServerTokenController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorOAuthServerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorObjectGraph' => 'AbstractDirectedGraph',
'PhabricatorObjectHandle' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasContributorEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasDraftEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasFileEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasJiraIssueEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasSubscriberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasUnsubscriberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasWatcherEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectListQuery' => 'Phobject',
'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase',
'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver',
'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorObjectMentionedByObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectMentionsObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorObjectRelationship' => 'Phobject',
'PhabricatorObjectRelationshipList' => 'Phobject',
'PhabricatorObjectRelationshipSource' => 'Phobject',
'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorObjectSelectorDialog' => 'Phobject',
'PhabricatorObjectStatus' => 'Phobject',
'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery',
'PhabricatorOldWorldContentSource' => 'PhabricatorContentSource',
'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting',
'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorOptionGroupSetting' => 'PhabricatorSetting',
'PhabricatorOwnerPathQuery' => 'Phobject',
'PhabricatorOwnersApplication' => 'PhabricatorApplication',
'PhabricatorOwnersArchiveController' => 'PhabricatorOwnersController',
'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorOwnersConfiguredCustomField' => array(
'PhabricatorOwnersCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorOwnersController' => 'PhabricatorController',
'PhabricatorOwnersCustomField' => 'PhabricatorCustomField',
'PhabricatorOwnersCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorOwnersCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorOwnersCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
'PhabricatorOwnersDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOwnersDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController',
'PhabricatorOwnersEditController' => 'PhabricatorOwnersController',
'PhabricatorOwnersHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PhabricatorOwnersListController' => 'PhabricatorOwnersController',
'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO',
'PhabricatorOwnersPackage' => array(
'PhabricatorOwnersDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorOwnersPackageAuditingTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageAutoreviewTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorOwnersPackageDescriptionTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageDominionTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOwnersPackageFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorOwnersPackageFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorOwnersPackageFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
+ 'PhabricatorOwnersPackageIgnoredTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorOwnersPackageNameTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorOwnersPackageOwnersTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorOwnersPackagePathsTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackagePrimaryTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOwnersPackageRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOwnersPackageStatusTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase',
'PhabricatorOwnersPackageTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorOwnersPackageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorOwnersPackageTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO',
'PhabricatorOwnersPathContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController',
'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHID' => 'Phobject',
'PhabricatorPHIDConstants' => 'Phobject',
'PhabricatorPHIDExportField' => 'PhabricatorExportField',
'PhabricatorPHIDListEditField' => 'PhabricatorEditField',
'PhabricatorPHIDListEditType' => 'PhabricatorEditType',
'PhabricatorPHIDListExportField' => 'PhabricatorListExportField',
'PhabricatorPHIDMailStamp' => 'PhabricatorMailStamp',
'PhabricatorPHIDResolver' => 'Phobject',
'PhabricatorPHIDType' => 'Phobject',
'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase',
'PhabricatorPHIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorPHPASTApplication' => 'PhabricatorApplication',
'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPHPMailerConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHPPreflightSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPackagesApplication' => 'PhabricatorApplication',
'PhabricatorPackagesController' => 'PhabricatorController',
'PhabricatorPackagesCreatePublisherCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesDAO' => 'PhabricatorLiskDAO',
'PhabricatorPackagesEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPackagesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPackagesNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorPackagesPackage' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesPackageController' => 'PhabricatorPackagesController',
'PhabricatorPackagesPackageDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPackagesPackageDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPackageDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesPackageEditController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPackageEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesPackageEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesPackageKeyTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackageListController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPackageListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesPackageNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesPackageNameTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesPackagePublisherTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackageQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesPackageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesPackageTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesPackageViewController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPublisher' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesPublisherController' => 'PhabricatorPackagesController',
'PhabricatorPackagesPublisherDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPackagesPublisherDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesPublisherEditController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesPublisherEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesPublisherEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesPublisherKeyTransaction' => 'PhabricatorPackagesPublisherTransactionType',
'PhabricatorPackagesPublisherListController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesPublisherListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesPublisherNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesPublisherNameTransaction' => 'PhabricatorPackagesPublisherTransactionType',
'PhabricatorPackagesPublisherPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesPublisherQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesPublisherSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesPublisherTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesPublisherTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesPublisherTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesPublisherViewController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPackagesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPackagesTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPackagesVersion' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesVersionController' => 'PhabricatorPackagesController',
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesVersionEditController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesVersionEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesVersionEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesVersionListController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesVersionListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesVersionNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesVersionNameTransaction' => 'PhabricatorPackagesVersionTransactionType',
'PhabricatorPackagesVersionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesVersionPackageTransaction' => 'PhabricatorPackagesVersionTransactionType',
'PhabricatorPackagesVersionQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesVersionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesVersionTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesVersionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesVersionTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesView' => 'AphrontView',
'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorPasswordHasher' => 'Phobject',
'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorPasswordHasherUnavailableException' => 'Exception',
'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorPaste' => array(
'PhabricatorPasteDAO',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMentionableInterface',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorPasteApplication' => 'PhabricatorApplication',
'PhabricatorPasteArchiveController' => 'PhabricatorPasteController',
'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteController' => 'PhabricatorController',
'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
'PhabricatorPasteEditController' => 'PhabricatorPasteController',
'PhabricatorPasteEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPasteFilenameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorPasteLanguageTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteListController' => 'PhabricatorPasteController',
'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType',
'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPasteRawController' => 'PhabricatorPasteController',
'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPasteSnippet' => 'Phobject',
'PhabricatorPasteStatusTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPasteTitleTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPasteTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPasteViewController' => 'PhabricatorPasteController',
'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleApplication' => 'PhabricatorApplication',
'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
'PhabricatorPeopleBadgesProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleCommitsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
'PhabricatorPeopleCreateGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController',
'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController',
'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPeopleIconSet' => 'PhabricatorIconSet',
'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController',
'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController',
'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController',
'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPeopleNewController' => 'PhabricatorPeopleController',
'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorPeoplePictureProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleProfileBadgesController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileCommitsController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileImageWorkflow' => 'PhabricatorPeopleManagementWorkflow',
'PhabricatorPeopleProfileManageController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileRevisionsController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileTasksController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileViewController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController',
'PhabricatorPeopleRevisionsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleTasksProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController',
'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorPhameApplication' => 'PhabricatorApplication',
'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhluxApplication' => 'PhabricatorApplication',
'PhabricatorPholioApplication' => 'PhabricatorApplication',
'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPhortuneTestCase' => 'PhabricatorTestCase',
'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
'PhabricatorPhrictionApplication' => 'PhabricatorApplication',
'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhurlApplication' => 'PhabricatorApplication',
'PhabricatorPhurlConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhurlController' => 'PhabricatorController',
'PhabricatorPhurlDAO' => 'PhabricatorLiskDAO',
'PhabricatorPhurlLinkRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorPhurlRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorPhurlSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPhurlShortURLController' => 'PhabricatorPhurlController',
'PhabricatorPhurlShortURLDefaultController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURL' => array(
'PhabricatorPhurlDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPhurlURLAccessController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLAliasTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLCreateCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPhurlURLDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPhurlURLDescriptionTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPhurlURLEditController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPhurlURLEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPhurlURLListController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLLongURLTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorPhurlURLNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorPhurlURLNameTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhurlURLQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPhurlURLReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorPhurlURLSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPhurlURLSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPhurlURLTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPhurlURLTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorPhurlURLTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPhurlURLTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController',
'PhabricatorPinnedApplicationsSetting' => 'PhabricatorInternalSetting',
'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation',
'PhabricatorPlatformSite' => 'PhabricatorSite',
'PhabricatorPointsEditField' => 'PhabricatorEditField',
'PhabricatorPointsFact' => 'PhabricatorFact',
'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
'PhabricatorPolicy' => array(
'PhabricatorPolicyDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorPolicyApplication' => 'PhabricatorApplication',
'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery',
'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanInteractCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCapability' => 'Phobject',
'PhabricatorPolicyCapabilityTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyCodex' => 'Phobject',
'PhabricatorPolicyCodexRuleDescription' => 'Phobject',
'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPolicyConstants' => 'Phobject',
'PhabricatorPolicyController' => 'PhabricatorController',
'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO',
'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyEditController' => 'PhabricatorPolicyController',
'PhabricatorPolicyEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorPolicyEditField' => 'PhabricatorEditField',
'PhabricatorPolicyException' => 'Exception',
'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
'PhabricatorPolicyFavoritesSetting' => 'PhabricatorInternalSetting',
'PhabricatorPolicyFilter' => 'Phobject',
'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface',
'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType',
'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorPolicyRule' => 'Phobject',
'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension',
+ 'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants',
'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyTestObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
'PhabricatorPonderApplication' => 'PhabricatorApplication',
'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProfileMenuEngine' => 'Phobject',
'PhabricatorProfileMenuItem' => 'Phobject',
'PhabricatorProfileMenuItemConfiguration' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorProfileMenuItemConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProfileMenuItemConfigurationTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProfileMenuItemIconSet' => 'PhabricatorIconSet',
'PhabricatorProfileMenuItemPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProject' => array(
'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorColumnProxyInterface',
+ 'PhabricatorSpacesInterface',
),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication',
'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardBackgroundController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample',
'PhabricatorProjectCardView' => 'AphrontTagView',
'PhabricatorProjectColorTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectColorsConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectColumn' => array(
'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProjectColumnPosition' => array(
'PhabricatorProjectDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorProjectConfiguredCustomField' => array(
'PhabricatorProjectStandardCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectController' => 'PhabricatorController',
'PhabricatorProjectCoreTestCase' => 'PhabricatorTestCase',
'PhabricatorProjectCoverController' => 'PhabricatorProjectController',
'PhabricatorProjectCustomField' => 'PhabricatorCustomField',
'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorProjectDAO' => 'PhabricatorLiskDAO',
'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectEditController' => 'PhabricatorProjectController',
'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
'PhabricatorProjectFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorProjectFilterTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorProjectHeraldAction' => 'HeraldAction',
'PhabricatorProjectHeraldAdapter' => 'HeraldAdapter',
'PhabricatorProjectHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PhabricatorProjectIconSet' => 'PhabricatorIconSet',
'PhabricatorProjectIconTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectIconsConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectImageTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectListController' => 'PhabricatorProjectController',
'PhabricatorProjectListView' => 'AphrontView',
'PhabricatorProjectLockController' => 'PhabricatorProjectController',
'PhabricatorProjectLockTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalOnlyDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectManageController' => 'PhabricatorProjectController',
'PhabricatorProjectManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectMaterializedMemberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMemberListView' => 'PhabricatorProjectUserListView',
'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMembersAddController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectMembersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectMembersProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController',
'PhabricatorProjectMenuItemController' => 'PhabricatorProjectController',
'PhabricatorProjectMilestoneTransaction' => 'PhabricatorProjectTypeTransaction',
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorProjectNameTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorProjectParentTransaction' => 'PhabricatorProjectTypeTransaction',
'PhabricatorProjectPictureProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectPointsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
'PhabricatorProjectProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorProjectProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectRemoveHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorProjectSilenceController' => 'PhabricatorProjectController',
'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
'PhabricatorProjectSlugsTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectSortTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectStandardCustomField' => array(
'PhabricatorProjectCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectStatus' => 'Phobject',
'PhabricatorProjectStatusTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectUserListView' => 'AphrontView',
'PhabricatorProjectViewController' => 'PhabricatorProjectController',
'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView',
'PhabricatorProjectWorkboardBackgroundColor' => 'Phobject',
'PhabricatorProjectWorkboardBackgroundTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectWorkboardProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectWorkboardTransaction' => 'PhabricatorProjectTransactionType',
+ 'PhabricatorProjectsAllPolicyRule' => 'PhabricatorProjectsBasePolicyRule',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
+ 'PhabricatorProjectsBasePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorProjectsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
- 'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule',
+ 'PhabricatorProjectsPolicyRule' => 'PhabricatorProjectsBasePolicyRule',
'PhabricatorProjectsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPronounSetting' => 'PhabricatorSelectSetting',
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
+ 'PhabricatorQueryIterator' => 'PhutilBufferedIterator',
'PhabricatorQueryOrderItem' => 'Phobject',
'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',
'PhabricatorQueryOrderVector' => array(
'Phobject',
'Iterator',
),
'PhabricatorQuickSearchEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
'PhabricatorRegexListConfigType' => 'PhabricatorTextListConfigType',
'PhabricatorRegistrationProfile' => 'Phobject',
'PhabricatorReleephApplication' => 'PhabricatorApplication',
'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRemarkupCachePurger' => 'PhabricatorCachePurger',
'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule',
'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule',
'PhabricatorRemarkupDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorRemarkupEditField' => 'PhabricatorEditField',
'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample',
'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorRepository' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMarkupInterface',
'PhabricatorDestructibleInterface',
'PhabricatorDestructibleCodexInterface',
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhabricatorRepositoryAuditRequest' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryCommit' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDraftInterface',
),
'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitHint' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryCommitRef' => 'Phobject',
'PhabricatorRepositoryCommitTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO',
'PhabricatorRepositoryDestructibleCodex' => 'PhabricatorDestructibleCodex',
'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorRepositoryEngine' => 'Phobject',
'PhabricatorRepositoryFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryGitLFSRef' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorRepositoryGitLFSRefQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryGraphCache' => 'Phobject',
'PhabricatorRepositoryGraphStream' => 'Phobject',
+ 'PhabricatorRepositoryIdentity' => array(
+ 'PhabricatorRepositoryDAO',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorApplicationTransactionInterface',
+ ),
+ 'PhabricatorRepositoryIdentityAssignTransaction' => 'PhabricatorRepositoryIdentityTransactionType',
+ 'PhabricatorRepositoryIdentityChangeWorker' => 'PhabricatorWorker',
+ 'PhabricatorRepositoryIdentityEditEngine' => 'PhabricatorEditEngine',
+ 'PhabricatorRepositoryIdentityFerretEngine' => 'PhabricatorFerretEngine',
+ 'PhabricatorRepositoryIdentityPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorRepositoryIdentityQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementHintWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMovePathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
+ 'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementThawWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUnpublishWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryMirror' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryOldRef' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryParsedChange' => 'Phobject',
'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryPullEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPullEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPullEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon',
'PhabricatorRepositoryPullLocalDaemonModule' => 'PhutilDaemonOverseerModule',
'PhabricatorRepositoryPushEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPushLog' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefCursor' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryRefCursorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryRefPosition' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryType' => 'Phobject',
'PhabricatorRepositoryURI' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURINormalizer' => 'Phobject',
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURIPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryURIQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO',
'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler',
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorSMS' => 'PhabricatorSMSDAO',
'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSMSDAO' => 'PhabricatorLiskDAO',
'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker',
'PhabricatorSMSImplementationAdapter' => 'Phobject',
'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter',
'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter',
'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow',
'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker',
'PhabricatorSMSWorker' => 'PhabricatorWorker',
'PhabricatorSQLPatchList' => 'Phobject',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSSHLog' => 'Phobject',
'PhabricatorSSHPassthruCommand' => 'Phobject',
'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorSavedQuery' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorScopedEnv' => 'Phobject',
'PhabricatorSearchAbstractDocument' => 'Phobject',
'PhabricatorSearchApplication' => 'PhabricatorApplication',
'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
'PhabricatorSearchConstraintException' => 'Exception',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchDateControlField' => 'PhabricatorSearchField',
'PhabricatorSearchDateField' => 'PhabricatorSearchField',
'PhabricatorSearchDefaultController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDocument' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentFieldType' => 'Phobject',
'PhabricatorSearchDocumentQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchEngineAPIMethod' => 'ConduitAPIMethod',
'PhabricatorSearchEngineAttachment' => 'Phobject',
'PhabricatorSearchEngineExtension' => 'Phobject',
'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorSearchFerretNgramGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSearchField' => 'Phobject',
'PhabricatorSearchHost' => 'Phobject',
'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementNgramsWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementQueryWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSearchNgrams' => 'PhabricatorSearchDAO',
'PhabricatorSearchNgramsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
'PhabricatorSearchRelationship' => 'Phobject',
'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchRelationshipSourceController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchResultBucket' => 'Phobject',
'PhabricatorSearchResultBucketGroup' => 'Phobject',
'PhabricatorSearchResultView' => 'AphrontView',
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
'PhabricatorSearchService' => 'Phobject',
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
'PhabricatorSearchThreeStateField' => 'PhabricatorSearchField',
'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField',
'PhabricatorSearchWorker' => 'PhabricatorWorker',
'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorSelectEditField' => 'PhabricatorEditField',
'PhabricatorSelectSetting' => 'PhabricatorSetting',
'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSetConfigType' => 'PhabricatorTextConfigType',
'PhabricatorSetting' => 'Phobject',
'PhabricatorSettingsAccountPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
'PhabricatorSettingsAdjustController' => 'PhabricatorController',
'PhabricatorSettingsApplication' => 'PhabricatorApplication',
'PhabricatorSettingsApplicationsPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsAuthenticationPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsDeveloperPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsEditEngine' => 'PhabricatorEditEngine',
'PhabricatorSettingsEmailPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsIssueController' => 'PhabricatorController',
'PhabricatorSettingsListController' => 'PhabricatorController',
'PhabricatorSettingsLogsPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsMainController' => 'PhabricatorController',
'PhabricatorSettingsPanel' => 'Phobject',
'PhabricatorSettingsPanelGroup' => 'Phobject',
'PhabricatorSettingsTimezoneController' => 'PhabricatorController',
'PhabricatorSetupCheck' => 'Phobject',
'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase',
'PhabricatorSetupEngine' => 'Phobject',
'PhabricatorSetupIssue' => 'Phobject',
'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
'PhabricatorSetupIssueView' => 'AphrontView',
'PhabricatorShortSite' => 'PhabricatorSite',
'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting',
'PhabricatorSimpleEditType' => 'PhabricatorEditType',
'PhabricatorSite' => 'AphrontSite',
'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorSlowvoteApplication' => 'PhabricatorApplication',
'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteCloseTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteController' => 'PhabricatorController',
'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO',
'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorSlowvoteDescriptionTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteEditController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvotePoll' => array(
'PhabricatorSlowvoteDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
),
'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType',
'PhabricatorSlowvoteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorSlowvoteQuestionTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorSlowvoteResponsesTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSlowvoteShuffleTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteTransaction' => 'PhabricatorModularTransaction',
'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorSlowvoteTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController',
'PhabricatorSlug' => 'Phobject',
'PhabricatorSlugTestCase' => 'PhabricatorTestCase',
'PhabricatorSourceCodeView' => 'AphrontView',
'PhabricatorSourceDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorSpaceEditField' => 'PhabricatorEditField',
'PhabricatorSpacesApplication' => 'PhabricatorApplication',
'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController',
'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesController' => 'PhabricatorController',
'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO',
'PhabricatorSpacesEditController' => 'PhabricatorSpacesController',
'PhabricatorSpacesExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface',
'PhabricatorSpacesListController' => 'PhabricatorSpacesController',
'PhabricatorSpacesMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSpacesNamespace' => array(
'PhabricatorSpacesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorSpacesNamespaceArchiveTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorSpacesNamespaceDefaultTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceDescriptionTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorSpacesNamespaceNameTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespacePHIDType' => 'PhabricatorPHIDType',
'PhabricatorSpacesNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorSpacesNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSpacesNamespaceTransaction' => 'PhabricatorModularTransaction',
'PhabricatorSpacesNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorSpacesNamespaceTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorSpacesNoAccessController' => 'PhabricatorSpacesController',
'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSpacesSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorSpacesSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSpacesTestCase' => 'PhabricatorTestCase',
'PhabricatorSpacesViewController' => 'PhabricatorSpacesController',
'PhabricatorStandardCustomField' => 'PhabricatorCustomField',
'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldTokenizer' => 'PhabricatorStandardCustomFieldPHIDs',
'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardPageView' => array(
'PhabricatorBarePageView',
'AphrontResponseProducerInterface',
),
'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorStaticEditField' => 'PhabricatorEditField',
'PhabricatorStatusController' => 'PhabricatorController',
'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
'PhabricatorStorageFixtureScopeGuard' => 'Phobject',
'PhabricatorStorageManagementAPI' => 'Phobject',
'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementAnalyzeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementOptimizeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementPartitionWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorStoragePatch' => 'Phobject',
'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorStringConfigType' => 'PhabricatorTextConfigType',
'PhabricatorStringExportField' => 'PhabricatorExportField',
'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType',
'PhabricatorStringListEditField' => 'PhabricatorEditField',
'PhabricatorStringListExportField' => 'PhabricatorListExportField',
'PhabricatorStringMailStamp' => 'PhabricatorMailStamp',
'PhabricatorStringSetting' => 'PhabricatorSetting',
'PhabricatorSubmitEditField' => 'PhabricatorEditField',
'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorSubscribersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication',
'PhabricatorSubscriptionsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
'PhabricatorSubscriptionsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorSubscriptionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction',
'PhabricatorSubscriptionsListController' => 'PhabricatorController',
'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSubscriptionsMuteController' => 'PhabricatorController',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorSubscriptionsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController',
'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubtypeEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorSupportApplication' => 'PhabricatorApplication',
'PhabricatorSyntaxHighlighter' => 'Phobject',
'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSyntaxStyle' => 'Phobject',
'PhabricatorSystemAction' => 'Phobject',
'PhabricatorSystemActionEngine' => 'Phobject',
'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO',
'PhabricatorSystemActionRateLimitException' => 'Exception',
'PhabricatorSystemApplication' => 'PhabricatorApplication',
'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
+ 'PhabricatorSystemObjectController' => 'PhabricatorController',
'PhabricatorSystemReadOnlyController' => 'PhabricatorController',
'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
'PhabricatorTaskmasterDaemonModule' => 'PhutilDaemonOverseerModule',
'PhabricatorTestApplication' => 'PhabricatorApplication',
'PhabricatorTestCase' => 'PhutilTestCase',
'PhabricatorTestController' => 'PhabricatorController',
'PhabricatorTestDataGenerator' => 'Phobject',
'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType',
'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorTestWorker' => 'PhabricatorWorker',
'PhabricatorTextAreaEditField' => 'PhabricatorEditField',
'PhabricatorTextConfigType' => 'PhabricatorConfigType',
'PhabricatorTextDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorTextEditField' => 'PhabricatorEditField',
'PhabricatorTextExportFormat' => 'PhabricatorExportFormat',
'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType',
'PhabricatorTime' => 'Phobject',
'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorTimeGuard' => 'Phobject',
'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'PhabricatorInternalSetting',
'PhabricatorTimezoneSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorTitleGlyphsSetting' => 'PhabricatorSelectSetting',
'PhabricatorToken' => array(
'PhabricatorTokenDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorTokenController' => 'PhabricatorController',
'PhabricatorTokenCount' => 'PhabricatorTokenDAO',
'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery',
'PhabricatorTokenDAO' => 'PhabricatorLiskDAO',
'PhabricatorTokenDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorTokenGiveController' => 'PhabricatorTokenController',
'PhabricatorTokenGiven' => array(
'PhabricatorTokenDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorTokenGivenController' => 'PhabricatorTokenController',
'PhabricatorTokenGivenEditor' => 'PhabricatorEditor',
'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory',
'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenLeaderController' => 'PhabricatorTokenController',
'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType',
'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener',
'PhabricatorTokenizerEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorTokensApplication' => 'PhabricatorApplication',
'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorTokensToken' => array(
'PhabricatorTokenDAO',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorTransactionChange' => 'Phobject',
'PhabricatorTransactionFactEngine' => 'PhabricatorFactEngine',
'PhabricatorTransactionRemarkupChange' => 'PhabricatorTransactionChange',
'PhabricatorTransactions' => 'Phobject',
'PhabricatorTransactionsApplication' => 'PhabricatorApplication',
'PhabricatorTransactionsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorTransactionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorTransformedFile' => 'PhabricatorFileDAO',
'PhabricatorTranslationSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorTriggerAction' => 'Phobject',
'PhabricatorTriggerClock' => 'Phobject',
'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase',
'PhabricatorTriggerDaemon' => 'PhabricatorDaemon',
'PhabricatorTrivialTestCase' => 'PhabricatorTestCase',
'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorTypeaheadApplication' => 'PhabricatorApplication',
'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadDatasource' => 'Phobject',
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorTypeaheadDatasourceTestCase' => 'PhabricatorTestCase',
'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadInvalidTokenException' => 'Exception',
'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadProxyDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorTypeaheadResult' => 'Phobject',
'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorTypeaheadTestNumbersDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadTokenView' => 'AphrontTagView',
'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUIExample' => 'Phobject',
'PhabricatorUIExampleRenderController' => 'PhabricatorController',
'PhabricatorUIExamplesApplication' => 'PhabricatorApplication',
'PhabricatorURIExportField' => 'PhabricatorExportField',
'PhabricatorUSEnglishTranslation' => 'PhutilTranslation',
'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting',
'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource',
'PhabricatorUnitsTestCase' => 'PhabricatorTestCase',
'PhabricatorUnknownContentSource' => 'PhabricatorContentSource',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorUser' => array(
'PhabricatorUserDAO',
'PhutilPerson',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSSHPublicKeyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorAuthPasswordHashInterface',
),
'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserCache' => 'PhabricatorUserDAO',
'PhabricatorUserCachePurger' => 'PhabricatorCachePurger',
'PhabricatorUserCacheType' => 'Phobject',
'PhabricatorUserCardView' => 'AphrontTagView',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUserConfiguredCustomField' => array(
'PhabricatorUserCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorUserCustomField' => 'PhabricatorCustomField',
'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
+ 'PhabricatorUserDisableTransaction' => 'PhabricatorUserTransactionType',
+ 'PhabricatorUserEditEngine' => 'PhabricatorEditEngine',
'PhabricatorUserEditor' => 'PhabricatorEditor',
'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',
'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase',
'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorUserFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorUserIconField' => 'PhabricatorUserCustomField',
'PhabricatorUserLog' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorUserLogView' => 'AphrontView',
'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorUserPreferences' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorUserPreferencesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserPreferencesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserPreferencesPHIDType' => 'PhabricatorPHIDType',
'PhabricatorUserPreferencesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorUserPreferencesSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorUserPreferencesTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorUserPreferencesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorUserProfile' => 'PhabricatorUserDAO',
- 'PhabricatorUserProfileEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserProfileImageCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField',
'PhabricatorUserRolesField' => 'PhabricatorUserCustomField',
'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorUserSinceField' => 'PhabricatorUserCustomField',
'PhabricatorUserStatusField' => 'PhabricatorUserCustomField',
'PhabricatorUserTestCase' => 'PhabricatorTestCase',
'PhabricatorUserTitleField' => 'PhabricatorUserCustomField',
- 'PhabricatorUserTransaction' => 'PhabricatorApplicationTransaction',
+ 'PhabricatorUserTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorUserTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorUserTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorUsersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorVCSResponse' => 'AphrontResponse',
'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO',
'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation',
'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorVoidDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorWebContentSource' => 'PhabricatorContentSource',
'PhabricatorWebServerSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting',
'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorWorker' => 'Phobject',
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerBulkJob' => array(
'PhabricatorWorkerDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorWorkerBulkJobCreateWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorWorkerBulkJobPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerBulkJobQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorWorkerBulkJobSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorWorkerBulkJobTaskWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerBulkJobTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorWorkerBulkJobTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorWorkerBulkJobType' => 'Phobject',
'PhabricatorWorkerBulkJobWorker' => 'PhabricatorWorker',
'PhabricatorWorkerBulkTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
'PhabricatorWorkerDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerPermanentFailureException' => 'Exception',
'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorWorkerSingleBulkJobType' => 'PhabricatorWorkerBulkJobType',
'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
'PhabricatorWorkerTaskQuery' => 'PhabricatorQuery',
'PhabricatorWorkerTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerTrigger' => array(
'PhabricatorWorkerDAO',
'PhabricatorDestructibleInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorWorkerTriggerEvent' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTriggerManagementFireWorkflow' => 'PhabricatorWorkerTriggerManagementWorkflow',
'PhabricatorWorkerTriggerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerTriggerPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerTriggerQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorWorkerYieldException' => 'Exception',
'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase',
'PhabricatorXHPASTDAO' => 'PhabricatorLiskDAO',
'PhabricatorXHPASTParseTree' => 'PhabricatorXHPASTDAO',
'PhabricatorXHPASTViewController' => 'PhabricatorController',
'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHProfApplication' => 'PhabricatorApplication',
'PhabricatorXHProfController' => 'PhabricatorController',
'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO',
'PhabricatorXHProfDropController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileView' => 'AphrontView',
'PhabricatorXHProfSample' => array(
'PhabricatorXHProfDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController',
'PhabricatorXHProfSampleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorXHProfSampleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule',
'Phame404Response' => 'AphrontHTMLResponse',
'PhameBlog' => array(
'PhameDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhameBlog404Controller' => 'PhameLiveController',
'PhameBlogArchiveController' => 'PhameBlogController',
'PhameBlogController' => 'PhameController',
'PhameBlogCreateCapability' => 'PhabricatorPolicyCapability',
'PhameBlogDatasource' => 'PhabricatorTypeaheadDatasource',
'PhameBlogDescriptionTransaction' => 'PhameBlogTransactionType',
'PhameBlogEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhameBlogEditController' => 'PhameBlogController',
'PhameBlogEditEngine' => 'PhabricatorEditEngine',
'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor',
'PhameBlogFeedController' => 'PhameBlogController',
'PhameBlogFerretEngine' => 'PhabricatorFerretEngine',
'PhameBlogFullDomainTransaction' => 'PhameBlogTransactionType',
'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine',
'PhameBlogHeaderImageTransaction' => 'PhameBlogTransactionType',
'PhameBlogHeaderPictureController' => 'PhameBlogController',
'PhameBlogListController' => 'PhameBlogController',
'PhameBlogListView' => 'AphrontTagView',
'PhameBlogManageController' => 'PhameBlogController',
'PhameBlogNameTransaction' => 'PhameBlogTransactionType',
'PhameBlogParentDomainTransaction' => 'PhameBlogTransactionType',
'PhameBlogParentSiteTransaction' => 'PhameBlogTransactionType',
'PhameBlogProfileImageTransaction' => 'PhameBlogTransactionType',
'PhameBlogProfilePictureController' => 'PhameBlogController',
'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhameBlogReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhameBlogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhameBlogSite' => 'PhameSite',
'PhameBlogStatusTransaction' => 'PhameBlogTransactionType',
'PhameBlogSubtitleTransaction' => 'PhameBlogTransactionType',
'PhameBlogTransaction' => 'PhabricatorModularTransaction',
'PhameBlogTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhameBlogTransactionType' => 'PhabricatorModularTransactionType',
'PhameBlogViewController' => 'PhameLiveController',
'PhameConstants' => 'Phobject',
'PhameController' => 'PhabricatorController',
'PhameDAO' => 'PhabricatorLiskDAO',
'PhameDescriptionView' => 'AphrontTagView',
'PhameDraftListView' => 'AphrontTagView',
'PhameHomeController' => 'PhamePostController',
'PhameLiveController' => 'PhameController',
'PhameNextPostView' => 'AphrontTagView',
'PhamePost' => array(
'PhameDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhamePostArchiveController' => 'PhamePostController',
'PhamePostBlogTransaction' => 'PhamePostTransactionType',
'PhamePostBodyTransaction' => 'PhamePostTransactionType',
'PhamePostController' => 'PhameController',
'PhamePostEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhamePostEditController' => 'PhamePostController',
'PhamePostEditEngine' => 'PhabricatorEditEngine',
'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor',
'PhamePostFerretEngine' => 'PhabricatorFerretEngine',
'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine',
'PhamePostHeaderImageTransaction' => 'PhamePostTransactionType',
'PhamePostHeaderPictureController' => 'PhamePostController',
'PhamePostHistoryController' => 'PhamePostController',
'PhamePostListController' => 'PhamePostController',
'PhamePostListView' => 'AphrontTagView',
'PhamePostMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhamePostMoveController' => 'PhamePostController',
'PhamePostPublishController' => 'PhamePostController',
'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhamePostRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhamePostReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhamePostSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhamePostSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhamePostSubtitleTransaction' => 'PhamePostTransactionType',
'PhamePostTitleTransaction' => 'PhamePostTransactionType',
'PhamePostTransaction' => 'PhabricatorModularTransaction',
'PhamePostTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhamePostTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhamePostTransactionType' => 'PhabricatorModularTransactionType',
'PhamePostViewController' => 'PhameLiveController',
'PhamePostVisibilityTransaction' => 'PhamePostTransactionType',
'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhameSite' => 'PhabricatorSite',
'PhluxController' => 'PhabricatorController',
'PhluxDAO' => 'PhabricatorLiskDAO',
'PhluxEditController' => 'PhluxController',
'PhluxListController' => 'PhluxController',
+ 'PhluxSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhluxTransaction' => 'PhabricatorApplicationTransaction',
'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhluxVariable' => array(
'PhluxDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
),
'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor',
'PhluxVariablePHIDType' => 'PhabricatorPHIDType',
'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhluxViewController' => 'PhluxController',
'PholioController' => 'PhabricatorController',
'PholioDAO' => 'PhabricatorLiskDAO',
'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PholioImage' => array(
'PholioDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
),
'PholioImageDescriptionTransaction' => 'PholioImageTransactionType',
'PholioImageFileTransaction' => 'PholioImageTransactionType',
'PholioImageNameTransaction' => 'PholioImageTransactionType',
'PholioImagePHIDType' => 'PhabricatorPHIDType',
'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PholioImageReplaceTransaction' => 'PholioImageTransactionType',
'PholioImageSequenceTransaction' => 'PholioImageTransactionType',
'PholioImageTransactionType' => 'PholioTransactionType',
'PholioImageUploadController' => 'PholioController',
'PholioInlineController' => 'PholioController',
'PholioInlineListController' => 'PholioController',
'PholioMock' => array(
'PholioDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorMentionableInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PholioMockArchiveController' => 'PholioController',
'PholioMockAuthorHeraldField' => 'PholioMockHeraldField',
'PholioMockCommentController' => 'PholioController',
'PholioMockDescriptionHeraldField' => 'PholioMockHeraldField',
'PholioMockDescriptionTransaction' => 'PholioMockTransactionType',
'PholioMockEditController' => 'PholioController',
'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor',
'PholioMockEmbedView' => 'AphrontView',
'PholioMockFerretEngine' => 'PhabricatorFerretEngine',
'PholioMockFulltextEngine' => 'PhabricatorFulltextEngine',
'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType',
'PholioMockHasTaskRelationship' => 'PholioMockRelationship',
'PholioMockHeraldField' => 'HeraldField',
'PholioMockHeraldFieldGroup' => 'HeraldFieldGroup',
'PholioMockImagesView' => 'AphrontView',
'PholioMockInlineTransaction' => 'PholioMockTransactionType',
'PholioMockListController' => 'PholioController',
'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver',
'PholioMockNameHeraldField' => 'PholioMockHeraldField',
'PholioMockNameTransaction' => 'PholioMockTransactionType',
'PholioMockPHIDType' => 'PhabricatorPHIDType',
'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PholioMockRelationship' => 'PhabricatorObjectRelationship',
'PholioMockRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PholioMockStatusTransaction' => 'PholioMockTransactionType',
'PholioMockThumbGridView' => 'AphrontView',
'PholioMockTransactionType' => 'PholioTransactionType',
'PholioMockViewController' => 'PholioController',
'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PholioTransaction' => 'PhabricatorModularTransaction',
'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PholioTransactionType' => 'PhabricatorModularTransactionType',
'PholioTransactionView' => 'PhabricatorApplicationTransactionView',
'PholioUploadedImageView' => 'AphrontView',
'PhortuneAccount' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneAccountAddManagerController' => 'PhortuneController',
'PhortuneAccountBillingController' => 'PhortuneAccountProfileController',
'PhortuneAccountChargeListController' => 'PhortuneController',
'PhortuneAccountController' => 'PhortuneController',
'PhortuneAccountEditController' => 'PhortuneController',
'PhortuneAccountEditEngine' => 'PhabricatorEditEngine',
'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneAccountListController' => 'PhortuneController',
'PhortuneAccountManagerController' => 'PhortuneAccountProfileController',
'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
'PhortuneAccountProfileController' => 'PhortuneAccountController',
'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController',
'PhortuneAccountTransaction' => 'PhabricatorModularTransaction',
'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneAccountViewController' => 'PhortuneAccountProfileController',
'PhortuneAdHocCart' => 'PhortuneCartImplementation',
'PhortuneAdHocProduct' => 'PhortuneProductImplementation',
'PhortuneCart' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneCartAcceptController' => 'PhortuneCartController',
'PhortuneCartCancelController' => 'PhortuneCartController',
'PhortuneCartCheckoutController' => 'PhortuneCartController',
'PhortuneCartController' => 'PhortuneController',
'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneCartImplementation' => 'Phobject',
'PhortuneCartListController' => 'PhortuneController',
'PhortuneCartPHIDType' => 'PhabricatorPHIDType',
'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneCartTransaction' => 'PhabricatorApplicationTransaction',
'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneCartUpdateController' => 'PhortuneCartController',
'PhortuneCartViewController' => 'PhortuneCartController',
'PhortuneCharge' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneChargePHIDType' => 'PhabricatorPHIDType',
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneChargeTableView' => 'AphrontView',
'PhortuneConstants' => 'Phobject',
'PhortuneController' => 'PhabricatorController',
'PhortuneCreditCardForm' => 'Phobject',
'PhortuneCurrency' => 'Phobject',
'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneInvoiceView' => 'AphrontTagView',
'PhortuneLandingController' => 'PhortuneController',
'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchant' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneMerchantAddManagerController' => 'PhortuneController',
'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantController' => 'PhortuneController',
'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantEditController' => 'PhortuneMerchantController',
'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine',
'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantListController' => 'PhortuneMerchantController',
'PhortuneMerchantManagerController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
'PhortuneMerchantPictureController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantProfileController' => 'PhortuneController',
'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction',
'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneMerchantViewController' => 'PhortuneMerchantProfileController',
'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
'PhortuneOrderTableView' => 'AphrontView',
'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider',
'PhortunePaymentMethod' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePaymentMethodCreateController' => 'PhortuneController',
'PhortunePaymentMethodDisableController' => 'PhortuneController',
'PhortunePaymentMethodEditController' => 'PhortuneController',
'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentProvider' => 'Phobject',
'PhortunePaymentProviderConfig' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase',
'PhortuneProduct' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneProductImplementation' => 'Phobject',
'PhortuneProductListController' => 'PhabricatorController',
'PhortuneProductPHIDType' => 'PhabricatorPHIDType',
'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneProductViewController' => 'PhortuneController',
'PhortuneProviderActionController' => 'PhortuneController',
'PhortuneProviderDisableController' => 'PhortuneMerchantController',
'PhortuneProviderEditController' => 'PhortuneMerchantController',
'PhortunePurchase' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePurchasePHIDType' => 'PhabricatorPHIDType',
'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
'PhortuneSubscription' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
'PhortuneSubscriptionEditController' => 'PhortuneController',
'PhortuneSubscriptionImplementation' => 'Phobject',
'PhortuneSubscriptionListController' => 'PhortuneController',
'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneSubscriptionTableView' => 'AphrontView',
'PhortuneSubscriptionViewController' => 'PhortuneController',
'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
'PhragmentBrowseController' => 'PhragmentController',
'PhragmentCanCreateCapability' => 'PhabricatorPolicyCapability',
'PhragmentConduitAPIMethod' => 'ConduitAPIMethod',
'PhragmentController' => 'PhabricatorController',
'PhragmentCreateController' => 'PhragmentController',
'PhragmentDAO' => 'PhabricatorLiskDAO',
'PhragmentFragment' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentFragmentPHIDType' => 'PhabricatorPHIDType',
'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentFragmentVersion' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentFragmentVersionPHIDType' => 'PhabricatorPHIDType',
'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentGetPatchConduitAPIMethod' => 'PhragmentConduitAPIMethod',
'PhragmentHistoryController' => 'PhragmentController',
'PhragmentPatchController' => 'PhragmentController',
'PhragmentPatchUtil' => 'Phobject',
'PhragmentPolicyController' => 'PhragmentController',
'PhragmentQueryFragmentsConduitAPIMethod' => 'PhragmentConduitAPIMethod',
'PhragmentRevertController' => 'PhragmentController',
'PhragmentSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhragmentSnapshot' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentSnapshotChild' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentSnapshotCreateController' => 'PhragmentController',
'PhragmentSnapshotDeleteController' => 'PhragmentController',
'PhragmentSnapshotPHIDType' => 'PhabricatorPHIDType',
'PhragmentSnapshotPromoteController' => 'PhragmentController',
'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentSnapshotViewController' => 'PhragmentController',
'PhragmentUpdateController' => 'PhragmentController',
'PhragmentVersionController' => 'PhragmentController',
'PhragmentZIPController' => 'PhragmentController',
'PhrequentConduitAPIMethod' => 'ConduitAPIMethod',
'PhrequentController' => 'PhabricatorController',
'PhrequentCurtainExtension' => 'PHUICurtainExtension',
'PhrequentDAO' => 'PhabricatorLiskDAO',
'PhrequentListController' => 'PhrequentController',
'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentPushConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrequentTimeBlock' => 'Phobject',
'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
'PhrequentTimeSlices' => 'Phobject',
'PhrequentTrackController' => 'PhrequentController',
'PhrequentTrackingConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentTrackingEditor' => 'PhabricatorEditor',
'PhrequentUIEventListener' => 'PhabricatorEventListener',
'PhrequentUserTime' => array(
'PhrequentDAO',
'PhabricatorPolicyInterface',
),
'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionChangeType' => 'PhrictionConstants',
'PhrictionConduitAPIMethod' => 'ConduitAPIMethod',
'PhrictionConstants' => 'Phobject',
'PhrictionContent' => array(
'PhrictionDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhrictionContentPHIDType' => 'PhabricatorPHIDType',
'PhrictionContentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionContentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhrictionContentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrictionContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhrictionController' => 'PhabricatorController',
'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionDAO' => 'PhabricatorLiskDAO',
'PhrictionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhrictionDeleteController' => 'PhrictionController',
'PhrictionDiffController' => 'PhrictionController',
'PhrictionDocument' => array(
'PhrictionDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
+ 'PhabricatorPolicyCodexInterface',
+ 'PhabricatorSpacesInterface',
),
'PhrictionDocumentAuthorHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentHeraldField' => 'PhrictionDocumentHeraldField',
- 'PhrictionDocumentContentTransaction' => 'PhrictionDocumentTransactionType',
+ 'PhrictionDocumentContentTransaction' => 'PhrictionDocumentEditTransaction',
'PhrictionDocumentController' => 'PhrictionController',
'PhrictionDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
- 'PhrictionDocumentDeleteTransaction' => 'PhrictionDocumentTransactionType',
+ 'PhrictionDocumentDeleteTransaction' => 'PhrictionDocumentVersionTransaction',
+ 'PhrictionDocumentDraftTransaction' => 'PhrictionDocumentEditTransaction',
+ 'PhrictionDocumentEditEngine' => 'PhabricatorEditEngine',
+ 'PhrictionDocumentEditTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentFerretEngine' => 'PhabricatorFerretEngine',
'PhrictionDocumentFulltextEngine' => 'PhabricatorFulltextEngine',
'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter',
'PhrictionDocumentHeraldField' => 'HeraldField',
'PhrictionDocumentHeraldFieldGroup' => 'HeraldFieldGroup',
- 'PhrictionDocumentMoveAwayTransaction' => 'PhrictionDocumentTransactionType',
- 'PhrictionDocumentMoveToTransaction' => 'PhrictionDocumentTransactionType',
+ 'PhrictionDocumentMoveAwayTransaction' => 'PhrictionDocumentVersionTransaction',
+ 'PhrictionDocumentMoveToTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType',
'PhrictionDocumentPathHeraldField' => 'PhrictionDocumentHeraldField',
+ 'PhrictionDocumentPolicyCodex' => 'PhabricatorPolicyCodex',
+ 'PhrictionDocumentPublishTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionDocumentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhrictionDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrictionDocumentStatus' => 'PhabricatorObjectStatus',
'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField',
- 'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentTransactionType',
+ 'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhrictionDocumentVersionTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionEditController' => 'PhrictionController',
+ 'PhrictionEditEngineController' => 'PhrictionController',
'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionHistoryController' => 'PhrictionController',
'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionListController' => 'PhrictionController',
'PhrictionMarkupPreviewController' => 'PhabricatorController',
'PhrictionMoveController' => 'PhrictionController',
'PhrictionNewController' => 'PhrictionController',
+ 'PhrictionPublishController' => 'PhrictionController',
'PhrictionRemarkupRule' => 'PhutilRemarkupRule',
'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhrictionTransaction' => 'PhabricatorModularTransaction',
'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
'PonderAddAnswerView' => 'AphrontView',
'PonderAnswer' => array(
'PonderDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
),
'PonderAnswerCommentController' => 'PonderController',
'PonderAnswerContentTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerEditController' => 'PonderController',
'PonderAnswerEditor' => 'PonderEditor',
'PonderAnswerHistoryController' => 'PonderController',
'PonderAnswerMailReceiver' => 'PhabricatorObjectMailReceiver',
'PonderAnswerPHIDType' => 'PhabricatorPHIDType',
'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PonderAnswerQuestionIDTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PonderAnswerSaveController' => 'PonderController',
'PonderAnswerStatus' => 'PonderConstants',
'PonderAnswerStatusTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerTransaction' => 'PhabricatorModularTransaction',
'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PonderAnswerTransactionType' => 'PhabricatorModularTransactionType',
'PonderAnswerView' => 'AphrontTagView',
'PonderConstants' => 'Phobject',
'PonderController' => 'PhabricatorController',
'PonderDAO' => 'PhabricatorLiskDAO',
'PonderDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PonderEditor' => 'PhabricatorApplicationTransactionEditor',
'PonderFooterView' => 'AphrontTagView',
'PonderModerateCapability' => 'PhabricatorPolicyCapability',
'PonderQuestion' => array(
'PonderDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMarkupInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PonderQuestionAnswerTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionAnswerWikiTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionCommentController' => 'PonderController',
'PonderQuestionContentTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionCreateMailReceiver' => 'PhabricatorMailReceiver',
'PonderQuestionEditController' => 'PonderController',
'PonderQuestionEditEngine' => 'PhabricatorEditEngine',
'PonderQuestionEditor' => 'PonderEditor',
'PonderQuestionFerretEngine' => 'PhabricatorFerretEngine',
'PonderQuestionFulltextEngine' => 'PhabricatorFulltextEngine',
'PonderQuestionHistoryController' => 'PonderController',
'PonderQuestionListController' => 'PonderController',
'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver',
'PonderQuestionPHIDType' => 'PhabricatorPHIDType',
'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PonderQuestionStatus' => 'PonderConstants',
'PonderQuestionStatusController' => 'PonderController',
'PonderQuestionStatusTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionTitleTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionTransaction' => 'PhabricatorModularTransaction',
'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PonderQuestionTransactionType' => 'PhabricatorModularTransactionType',
'PonderQuestionViewController' => 'PonderController',
'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand',
'ProjectBoardTaskCard' => 'Phobject',
'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability',
'ProjectColumnSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
'ProjectDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability',
'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability',
'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability',
'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod',
'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase',
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'QueryFormattingTestCase' => 'PhabricatorTestCase',
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranch' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'ReleephBranchAccessController' => 'ReleephBranchController',
'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranchController' => 'ReleephController',
'ReleephBranchCreateController' => 'ReleephProductController',
'ReleephBranchEditController' => 'ReleephBranchController',
'ReleephBranchEditor' => 'PhabricatorEditor',
'ReleephBranchHistoryController' => 'ReleephBranchController',
'ReleephBranchNamePreviewController' => 'ReleephController',
'ReleephBranchPHIDType' => 'PhabricatorPHIDType',
'ReleephBranchPreviewView' => 'AphrontFormControl',
'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephBranchSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephBranchTemplate' => 'Phobject',
'ReleephBranchTransaction' => 'PhabricatorApplicationTransaction',
'ReleephBranchTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephBranchViewController' => 'ReleephBranchController',
'ReleephCommitFinder' => 'Phobject',
'ReleephCommitFinderException' => 'Exception',
'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephConduitAPIMethod' => 'ConduitAPIMethod',
'ReleephController' => 'PhabricatorController',
'ReleephDAO' => 'PhabricatorLiskDAO',
'ReleephDefaultFieldSelector' => 'ReleephFieldSelector',
'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification',
'ReleephFieldParseException' => 'Exception',
'ReleephFieldSelector' => 'Phobject',
'ReleephFieldSpecification' => array(
'PhabricatorCustomField',
'PhabricatorMarkupInterface',
),
'ReleephGetBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification',
'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification',
'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephProductActionController' => 'ReleephProductController',
'ReleephProductController' => 'ReleephController',
'ReleephProductCreateController' => 'ReleephProductController',
'ReleephProductEditController' => 'ReleephProductController',
'ReleephProductEditor' => 'PhabricatorApplicationTransactionEditor',
'ReleephProductHistoryController' => 'ReleephProductController',
'ReleephProductListController' => 'ReleephController',
'ReleephProductPHIDType' => 'PhabricatorPHIDType',
'ReleephProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephProductSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephProductTransaction' => 'PhabricatorApplicationTransaction',
'ReleephProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephProductViewController' => 'ReleephProductController',
'ReleephProject' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'ReleephQueryBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephQueryProductsConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephQueryRequestsConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRequest' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
),
'ReleephRequestActionController' => 'ReleephRequestController',
'ReleephRequestCommentController' => 'ReleephRequestController',
'ReleephRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephRequestController' => 'ReleephController',
'ReleephRequestDifferentialCreateController' => 'ReleephController',
'ReleephRequestEditController' => 'ReleephBranchController',
'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver',
'ReleephRequestPHIDType' => 'PhabricatorPHIDType',
'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephRequestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephRequestStatus' => 'Phobject',
'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction',
'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ReleephRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephRequestTransactionalEditor' => 'PhabricatorApplicationTransactionEditor',
'ReleephRequestTypeaheadControl' => 'AphrontFormControl',
'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController',
'ReleephRequestView' => 'AphrontView',
'ReleephRequestViewController' => 'ReleephBranchController',
'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification',
'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification',
'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
'ReleephWorkCanPushConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetBranchConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkNextRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod',
'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod',
'RepositoryConduitAPIMethod' => 'ConduitAPIMethod',
'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod',
'ShellLogView' => 'AphrontView',
'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod',
'SlowvoteEmbedView' => 'AphrontView',
'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod',
'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'SubscriptionListDialogBuilder' => 'Phobject',
'SubscriptionListStringBuilder' => 'Phobject',
'TokenConduitAPIMethod' => 'ConduitAPIMethod',
'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod',
'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod',
'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod',
'TransactionSearchConduitAPIMethod' => 'ConduitAPIMethod',
'UserConduitAPIMethod' => 'ConduitAPIMethod',
'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod',
+ 'UserEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod',
'UserFindConduitAPIMethod' => 'UserConduitAPIMethod',
'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod',
'UserSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod',
),
));
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
index fe1e80318..0eab91b2a 100644
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -1,426 +1,434 @@
<?php
abstract class AphrontResponse extends Phobject {
private $request;
private $cacheable = false;
private $canCDN;
private $responseCode = 200;
private $lastModified = null;
private $contentSecurityPolicyURIs;
private $disableContentSecurityPolicy;
protected $frameable;
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
final public function addContentSecurityPolicyURI($kind, $uri) {
if ($this->contentSecurityPolicyURIs === null) {
$this->contentSecurityPolicyURIs = array(
'script-src' => array(),
'connect-src' => array(),
'frame-src' => array(),
'form-action' => array(),
'object-src' => array(),
);
}
if (!isset($this->contentSecurityPolicyURIs[$kind])) {
throw new Exception(
pht(
'Unknown Content-Security-Policy URI kind "%s".',
$kind));
}
$this->contentSecurityPolicyURIs[$kind][] = (string)$uri;
return $this;
}
final public function setDisableContentSecurityPolicy($disable) {
$this->disableContentSecurityPolicy = $disable;
return $this;
}
/* -( Content )------------------------------------------------------------ */
public function getContentIterator() {
- return array($this->buildResponseString());
+ // By default, make sure responses are truly returning a string, not some
+ // kind of object that behaves like a string.
+
+ // We're going to remove the execution time limit before dumping the
+ // response into the sink, and want any rendering that's going to occur
+ // to happen BEFORE we release the limit.
+
+ return array(
+ (string)$this->buildResponseString(),
+ );
}
public function buildResponseString() {
throw new PhutilMethodNotImplementedException();
}
/* -( Metadata )----------------------------------------------------------- */
public function getHeaders() {
$headers = array();
if (!$this->frameable) {
$headers[] = array('X-Frame-Options', 'Deny');
}
if ($this->getRequest() && $this->getRequest()->isHTTPS()) {
$hsts_key = 'security.strict-transport-security';
$use_hsts = PhabricatorEnv::getEnvConfig($hsts_key);
if ($use_hsts) {
$duration = phutil_units('365 days in seconds');
} else {
// If HSTS has been disabled, tell browsers to turn it off. This may
// not be effective because we can only disable it over a valid HTTPS
// connection, but it best represents the configured intent.
$duration = 0;
}
$headers[] = array(
'Strict-Transport-Security',
"max-age={$duration}; includeSubdomains; preload",
);
}
$csp = $this->newContentSecurityPolicyHeader();
if ($csp !== null) {
$headers[] = array('Content-Security-Policy', $csp);
}
$headers[] = array('Referrer-Policy', 'no-referrer');
return $headers;
}
private function newContentSecurityPolicyHeader() {
if ($this->disableContentSecurityPolicy) {
return null;
}
// NOTE: We may return a response during preflight checks (for example,
// if a user has a bad version of PHP).
// In this case, setup isn't complete yet and we can't access environmental
// configuration. If we aren't able to read the environment, just decline
// to emit a Content-Security-Policy header.
try {
$cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
+ $base_uri = PhabricatorEnv::getURI('/');
} catch (Exception $ex) {
return null;
}
$csp = array();
if ($cdn) {
$default = $this->newContentSecurityPolicySource($cdn);
} else {
// If an alternate file domain is not configured and the user is viewing
// a Phame blog on a custom domain or some other custom site, we'll still
// serve resources from the main site. Include the main site explicitly.
-
- $base_uri = PhabricatorEnv::getURI('/');
$base_uri = $this->newContentSecurityPolicySource($base_uri);
$default = "'self' {$base_uri}";
}
$csp[] = "default-src {$default}";
// We use "data:" URIs to inline small images into CSS. This policy allows
// "data:" URIs to be used anywhere, but there doesn't appear to be a way
// to say that "data:" URIs are okay in CSS files but not in the document.
$csp[] = "img-src {$default} data:";
// We use inline style="..." attributes in various places, many of which
// are legitimate. We also currently use a <style> tag to implement the
// "Monospaced Font Preference" setting.
$csp[] = "style-src {$default} 'unsafe-inline'";
// On a small number of pages, including the Stripe workflow and the
// ReCAPTCHA challenge, we embed external Javascript directly.
$csp[] = $this->newContentSecurityPolicy('script-src', $default);
// We need to specify that we can connect to ourself in order for AJAX
// requests to work.
$csp[] = $this->newContentSecurityPolicy('connect-src', "'self'");
// DarkConsole and PHPAST both use frames to render some content.
$csp[] = $this->newContentSecurityPolicy('frame-src', "'self'");
// This is a more modern flavor of of "X-Frame-Options" and prevents
// clickjacking attacks where the page is included in a tiny iframe and
// the user is convinced to click a element on the page, which really
// clicks a dangerous button hidden under a picture of a cat.
if ($this->frameable) {
$csp[] = "frame-ancestors 'self'";
} else {
$csp[] = "frame-ancestors 'none'";
}
// Block relics of the old world: Flash, Java applets, and so on. Note
// that Chrome prevents the user from viewing PDF documents if they are
// served with a policy which excludes the domain they are served from.
$csp[] = $this->newContentSecurityPolicy('object-src', "'none'");
// Don't allow forms to submit offsite.
// This can result in some trickiness with file downloads if applications
// try to start downloads by submitting a dialog. Redirect to the file's
// download URI instead of submitting a form to it.
$csp[] = $this->newContentSecurityPolicy('form-action', "'self'");
// Block use of "<base>" to change the origin of relative URIs on the page.
$csp[] = "base-uri 'none'";
$csp = implode('; ', $csp);
return $csp;
}
private function newContentSecurityPolicy($type, $defaults) {
if ($defaults === null) {
$sources = array();
} else {
$sources = (array)$defaults;
}
$uris = $this->contentSecurityPolicyURIs;
if (isset($uris[$type])) {
foreach ($uris[$type] as $uri) {
$sources[] = $this->newContentSecurityPolicySource($uri);
}
}
$sources = array_unique($sources);
return $type.' '.implode(' ', $sources);
}
private function newContentSecurityPolicySource($uri) {
// Some CSP URIs are ultimately user controlled (like notification server
// URIs and CDN URIs) so attempt to stop an attacker from injecting an
// unsafe source (like 'unsafe-eval') into the CSP header.
$uri = id(new PhutilURI($uri))
->setPath(null)
->setFragment(null)
->setQueryParams(array());
$uri = (string)$uri;
if (preg_match('/[ ;\']/', $uri)) {
throw new Exception(
pht(
'Attempting to emit a response with an unsafe source ("%s") in the '.
'Content-Security-Policy header.',
$uri));
}
return $uri;
}
public function setCacheDurationInSeconds($duration) {
$this->cacheable = $duration;
return $this;
}
public function setCanCDN($can_cdn) {
$this->canCDN = $can_cdn;
return $this;
}
public function setLastModified($epoch_timestamp) {
$this->lastModified = $epoch_timestamp;
return $this;
}
public function setHTTPResponseCode($code) {
$this->responseCode = $code;
return $this;
}
public function getHTTPResponseCode() {
return $this->responseCode;
}
public function getHTTPResponseMessage() {
switch ($this->getHTTPResponseCode()) {
case 100: return 'Continue';
case 101: return 'Switching Protocols';
case 200: return 'OK';
case 201: return 'Created';
case 202: return 'Accepted';
case 203: return 'Non-Authoritative Information';
case 204: return 'No Content';
case 205: return 'Reset Content';
case 206: return 'Partial Content';
case 300: return 'Multiple Choices';
case 301: return 'Moved Permanently';
case 302: return 'Found';
case 303: return 'See Other';
case 304: return 'Not Modified';
case 305: return 'Use Proxy';
case 306: return 'Switch Proxy';
case 307: return 'Temporary Redirect';
case 400: return 'Bad Request';
case 401: return 'Unauthorized';
case 402: return 'Payment Required';
case 403: return 'Forbidden';
case 404: return 'Not Found';
case 405: return 'Method Not Allowed';
case 406: return 'Not Acceptable';
case 407: return 'Proxy Authentication Required';
case 408: return 'Request Timeout';
case 409: return 'Conflict';
case 410: return 'Gone';
case 411: return 'Length Required';
case 412: return 'Precondition Failed';
case 413: return 'Request Entity Too Large';
case 414: return 'Request-URI Too Long';
case 415: return 'Unsupported Media Type';
case 416: return 'Requested Range Not Satisfiable';
case 417: return 'Expectation Failed';
case 418: return "I'm a teapot";
case 426: return 'Upgrade Required';
case 500: return 'Internal Server Error';
case 501: return 'Not Implemented';
case 502: return 'Bad Gateway';
case 503: return 'Service Unavailable';
case 504: return 'Gateway Timeout';
case 505: return 'HTTP Version Not Supported';
default: return '';
}
}
public function setFrameable($frameable) {
$this->frameable = $frameable;
return $this;
}
public static function processValueForJSONEncoding(&$value, $key) {
if ($value instanceof PhutilSafeHTMLProducerInterface) {
// This renders the producer down to PhutilSafeHTML, which will then
// be simplified into a string below.
$value = hsprintf('%s', $value);
}
if ($value instanceof PhutilSafeHTML) {
// TODO: Javelin supports implicity conversion of '__html' objects to
// JX.HTML, but only for Ajax responses, not behaviors. Just leave things
// as they are for now (where behaviors treat responses as HTML or plain
// text at their discretion).
$value = $value->getHTMLContent();
}
}
public static function encodeJSONForHTTPResponse(array $object) {
array_walk_recursive(
$object,
array(__CLASS__, 'processValueForJSONEncoding'));
$response = phutil_json_encode($object);
// Prevent content sniffing attacks by encoding "<" and ">", so browsers
// won't try to execute the document as HTML even if they ignore
// Content-Type and X-Content-Type-Options. See T865.
$response = str_replace(
array('<', '>'),
array('\u003c', '\u003e'),
$response);
return $response;
}
protected function addJSONShield($json_response) {
// Add a shield to prevent "JSON Hijacking" attacks where an attacker
// requests a JSON response using a normal <script /> tag and then uses
// Object.prototype.__defineSetter__() or similar to read response data.
// This header causes the browser to loop infinitely instead of handing over
// sensitive data.
$shield = 'for (;;);';
$response = $shield.$json_response;
return $response;
}
public function getCacheHeaders() {
$headers = array();
if ($this->cacheable) {
$cache_control = array();
$cache_control[] = sprintf('max-age=%d', $this->cacheable);
if ($this->canCDN) {
$cache_control[] = 'public';
} else {
$cache_control[] = 'private';
}
$headers[] = array(
'Cache-Control',
implode(', ', $cache_control),
);
$headers[] = array(
'Expires',
$this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),
);
} else {
$headers[] = array(
'Cache-Control',
'no-store',
);
$headers[] = array(
'Expires',
'Sat, 01 Jan 2000 00:00:00 GMT',
);
}
if ($this->lastModified) {
$headers[] = array(
'Last-Modified',
$this->formatEpochTimestampForHTTPHeader($this->lastModified),
);
}
// IE has a feature where it may override an explicit Content-Type
// declaration by inferring a content type. This can be a security risk
// and we always explicitly transmit the correct Content-Type header, so
// prevent IE from using inferred content types. This only offers protection
// on recent versions of IE; IE6/7 and Opera currently ignore this header.
$headers[] = array('X-Content-Type-Options', 'nosniff');
return $headers;
}
private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {
return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
}
protected function shouldCompressResponse() {
return true;
}
public function willBeginWrite() {
if ($this->shouldCompressResponse()) {
// Enable automatic compression here. Webservers sometimes do this for
// us, but we now detect the absence of compression and warn users about
// it so try to cover our bases more thoroughly.
ini_set('zlib.output_compression', 1);
} else {
ini_set('zlib.output_compression', 0);
}
}
public function didCompleteWrite($aborted) {
return;
}
}
diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php
index 11c6466cf..51c54df52 100644
--- a/src/aphront/sink/AphrontHTTPSink.php
+++ b/src/aphront/sink/AphrontHTTPSink.php
@@ -1,136 +1,153 @@
<?php
/**
* Abstract class which wraps some sort of output mechanism for HTTP responses.
* Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and
* "header()" to emit responses.
*
* Mostly, this class allows us to do install security or metrics hooks in the
* output pipeline.
*
* @task write Writing Response Components
* @task emit Emitting the Response
*/
abstract class AphrontHTTPSink extends Phobject {
/* -( Writing Response Components )---------------------------------------- */
/**
* Write an HTTP status code to the output.
*
* @param int Numeric HTTP status code.
* @return void
*/
final public function writeHTTPStatus($code, $message = '') {
if (!preg_match('/^\d{3}$/', $code)) {
throw new Exception(pht("Malformed HTTP status code '%s'!", $code));
}
$code = (int)$code;
$this->emitHTTPStatus($code, $message);
}
/**
* Write HTTP headers to the output.
*
* @param list<pair> List of <name, value> pairs.
* @return void
*/
final public function writeHeaders(array $headers) {
foreach ($headers as $header) {
if (!is_array($header) || count($header) !== 2) {
throw new Exception(pht('Malformed header.'));
}
list($name, $value) = $header;
if (strpos($name, ':') !== false) {
throw new Exception(
pht(
'Declining to emit response with malformed HTTP header name: %s',
$name));
}
// Attackers may perform an "HTTP response splitting" attack by making
// the application emit certain types of headers containing newlines:
//
// http://en.wikipedia.org/wiki/HTTP_response_splitting
//
// PHP has built-in protections against HTTP response-splitting, but they
// are of dubious trustworthiness:
//
// http://news.php.net/php.internals/57655
if (preg_match('/[\r\n\0]/', $name.$value)) {
throw new Exception(
pht(
'Declining to emit response with unsafe HTTP header: %s',
"<'".$name."', '".$value."'>."));
}
}
foreach ($headers as $header) {
list($name, $value) = $header;
$this->emitHeader($name, $value);
}
}
/**
* Write HTTP body data to the output.
*
* @param string Body data.
* @return void
*/
final public function writeData($data) {
$this->emitData($data);
}
/**
* Write an entire @{class:AphrontResponse} to the output.
*
* @param AphrontResponse The response object to write.
* @return void
*/
final public function writeResponse(AphrontResponse $response) {
$response->willBeginWrite();
// Build the content iterator first, in case it throws. Ideally, we'd
// prefer to handle exceptions before we emit the response status or any
// HTTP headers.
$data = $response->getContentIterator();
$all_headers = array_merge(
$response->getHeaders(),
$response->getCacheHeaders());
$this->writeHTTPStatus(
$response->getHTTPResponseCode(),
$response->getHTTPResponseMessage());
$this->writeHeaders($all_headers);
+ // Allow clients an unlimited amount of time to download the response.
+
+ // This allows clients to perform a "slow loris" attack, where they
+ // download a large response very slowly to tie up process slots. However,
+ // concurrent connection limits and "RequestReadTimeout" already prevent
+ // this attack. We could add our own minimum download rate here if we want
+ // to make this easier to configure eventually.
+
+ // For normal page responses, we've fully rendered the page into a string
+ // already so all that's left is writing it to the client.
+
+ // For unusual responses (like large file downloads) we may still be doing
+ // some meaningful work, but in theory that work is intrinsic to streaming
+ // the response.
+
+ set_time_limit(0);
+
$abort = false;
foreach ($data as $block) {
if (!$this->isWritable()) {
$abort = true;
break;
}
$this->writeData($block);
}
$response->didCompleteWrite($abort);
}
/* -( Emitting the Response )---------------------------------------------- */
abstract protected function emitHTTPStatus($code, $message = '');
abstract protected function emitHeader($name, $value);
abstract protected function emitData($data);
abstract protected function isWritable();
}
diff --git a/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php b/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php
index 2c68ed037..30c371d6f 100644
--- a/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php
+++ b/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php
@@ -1,183 +1,186 @@
<?php
final class AlmanacInterfaceEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'almanac.interface';
private $device;
public function setDevice(AlmanacDevice $device) {
$this->device = $device;
return $this;
}
public function getDevice() {
if (!$this->device) {
throw new PhutilInvalidStateException('setDevice');
}
return $this->device;
}
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Almanac Interfaces');
}
public function getSummaryHeader() {
return pht('Edit Almanac Interface Configurations');
}
public function getSummaryText() {
return pht('This engine is used to edit Almanac interfaces.');
}
public function getEngineApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
protected function newEditableObject() {
$interface = AlmanacInterface::initializeNewInterface();
$device = $this->getDevice();
$interface
->setDevicePHID($device->getPHID())
->attachDevice($device);
return $interface;
}
protected function newEditableObjectForDocumentation() {
$this->setDevice(new AlmanacDevice());
return $this->newEditableObject();
}
protected function newEditableObjectFromConduit(array $raw_xactions) {
$device_phid = null;
foreach ($raw_xactions as $raw_xaction) {
if ($raw_xaction['type'] !== 'device') {
continue;
}
$device_phid = $raw_xaction['value'];
}
if ($device_phid === null) {
throw new Exception(
pht(
'When creating a new Almanac interface via the Conduit API, you '.
'must provide a "device" transaction to select a device.'));
}
$device = id(new AlmanacDeviceQuery())
->setViewer($this->getViewer())
->withPHIDs(array($device_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$device) {
throw new Exception(
pht(
'Device "%s" is unrecognized, restricted, or you do not have '.
'permission to edit it.',
$device_phid));
}
$this->setDevice($device);
return $this->newEditableObject();
}
protected function newObjectQuery() {
return new AlmanacInterfaceQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Interface');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Interface');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Interface');
}
protected function getObjectEditShortText($object) {
return pht('Edit Interface');
}
protected function getObjectCreateShortText() {
return pht('Create Interface');
}
protected function getObjectName() {
return pht('Interface');
}
protected function getEditorURI() {
return '/almanac/interface/edit/';
}
protected function getObjectCreateCancelURI($object) {
+ if ($this->getDevice()) {
+ return $this->getDevice()->getURI();
+ }
return '/almanac/interface/';
}
protected function getObjectViewURI($object) {
return $object->getDevice()->getURI();
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
// TODO: Some day, this should be a datasource.
$networks = id(new AlmanacNetworkQuery())
->setViewer($viewer)
->execute();
$network_map = mpull($networks, 'getName', 'getPHID');
return array(
id(new PhabricatorTextEditField())
->setKey('device')
->setLabel(pht('Device'))
->setIsConduitOnly(true)
->setTransactionType(
AlmanacInterfaceDeviceTransaction::TRANSACTIONTYPE)
->setDescription(pht('When creating an interface, set the device.'))
->setConduitDescription(pht('Set the device.'))
->setConduitTypeDescription(pht('Device PHID.'))
->setValue($object->getDevicePHID()),
id(new PhabricatorSelectEditField())
->setKey('network')
->setLabel(pht('Network'))
->setDescription(pht('Network for the interface.'))
->setTransactionType(
AlmanacInterfaceNetworkTransaction::TRANSACTIONTYPE)
->setValue($object->getNetworkPHID())
->setOptions($network_map),
id(new PhabricatorTextEditField())
->setKey('address')
->setLabel(pht('Address'))
->setDescription(pht('Address of the service.'))
->setTransactionType(
AlmanacInterfaceAddressTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getAddress()),
id(new PhabricatorIntEditField())
->setKey('port')
->setLabel(pht('Port'))
->setDescription(pht('Port of the service.'))
->setTransactionType(AlmanacInterfacePortTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getPort()),
);
}
}
diff --git a/src/applications/almanac/editor/AlmanacInterfaceEditor.php b/src/applications/almanac/editor/AlmanacInterfaceEditor.php
index 865402db3..771076efc 100644
--- a/src/applications/almanac/editor/AlmanacInterfaceEditor.php
+++ b/src/applications/almanac/editor/AlmanacInterfaceEditor.php
@@ -1,18 +1,36 @@
<?php
final class AlmanacInterfaceEditor
extends AlmanacEditor {
public function getEditorObjectsDescription() {
return pht('Almanac Interface');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this interface.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
+ protected function didCatchDuplicateKeyException(
+ PhabricatorLiskDAO $object,
+ array $xactions,
+ Exception $ex) {
+
+ $errors = array();
+
+ $errors[] = new PhabricatorApplicationTransactionValidationError(
+ null,
+ pht('Invalid'),
+ pht(
+ 'Interfaces must have a unique combination of network, device, '.
+ 'address, and port.'),
+ null);
+
+ throw new PhabricatorApplicationTransactionValidationException($errors);
+ }
+
}
diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php
index 29461bfbf..0d38070e0 100644
--- a/src/applications/almanac/query/AlmanacDeviceQuery.php
+++ b/src/applications/almanac/query/AlmanacDeviceQuery.php
@@ -1,133 +1,146 @@
<?php
final class AlmanacDeviceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
private $namePrefix;
private $nameSuffix;
+ private $isClusterDevice;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withNameSuffix($suffix) {
$this->nameSuffix = $suffix;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacDeviceNameNgrams(),
$ngrams);
}
+ public function withIsClusterDevice($is_cluster_device) {
+ $this->isClusterDevice = $is_cluster_device;
+ return $this;
+ }
+
public function newResultObject() {
return new AlmanacDevice();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'device.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'device.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$hashes = array();
foreach ($this->names as $name) {
$hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn,
'device.nameIndex IN (%Ls)',
$hashes);
}
if ($this->namePrefix !== null) {
$where[] = qsprintf(
$conn,
'device.name LIKE %>',
$this->namePrefix);
}
if ($this->nameSuffix !== null) {
$where[] = qsprintf(
$conn,
'device.name LIKE %<',
$this->nameSuffix);
}
+ if ($this->isClusterDevice !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'device.isBoundToClusterService = %d',
+ (int)$this->isClusterDevice);
+ }
+
return $where;
}
protected function getPrimaryTableAlias() {
return 'device';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$device = $this->loadCursorObject($cursor);
return array(
'id' => $device->getID(),
'name' => $device->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Device Name'),
),
) + parent::getBuiltinOrders();
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}
diff --git a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php
index 6e28d7e5b..1d0a1d847 100644
--- a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php
+++ b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php
@@ -1,101 +1,112 @@
<?php
final class AlmanacDeviceSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Almanac Devices');
}
public function getApplicationClassName() {
return 'PhabricatorAlmanacApplication';
}
public function newQuery() {
return new AlmanacDeviceQuery();
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchTextField())
->setLabel(pht('Name Contains'))
->setKey('match')
->setDescription(pht('Search for devices by name substring.')),
id(new PhabricatorSearchStringListField())
->setLabel(pht('Exact Names'))
->setKey('names')
->setDescription(pht('Search for devices with specific names.')),
+ id(new PhabricatorSearchThreeStateField())
+ ->setLabel(pht('Cluster Device'))
+ ->setKey('isClusterDevice')
+ ->setOptions(
+ pht('Both Cluster and Non-cluster Devices'),
+ pht('Cluster Devices Only'),
+ pht('Non-cluster Devices Only')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['match'] !== null) {
$query->withNameNgrams($map['match']);
}
if ($map['names']) {
$query->withNames($map['names']);
}
+ if ($map['isClusterDevice'] !== null) {
+ $query->withIsClusterDevice($map['isClusterDevice']);
+ }
+
return $query;
}
protected function getURI($path) {
return '/almanac/device/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Devices'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $devices,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($devices, 'AlmanacDevice');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$list->setUser($viewer);
foreach ($devices as $device) {
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Device %d', $device->getID()))
->setHeader($device->getName())
->setHref($device->getURI())
->setObject($device);
if ($device->isClusterDevice()) {
$item->addIcon('fa-sitemap', pht('Cluster Device'));
}
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No Almanac Devices found.'));
return $result;
}
}
diff --git a/src/applications/almanac/query/AlmanacNetworkQuery.php b/src/applications/almanac/query/AlmanacNetworkQuery.php
index a09da0093..13176f7de 100644
--- a/src/applications/almanac/query/AlmanacNetworkQuery.php
+++ b/src/applications/almanac/query/AlmanacNetworkQuery.php
@@ -1,61 +1,74 @@
<?php
final class AlmanacNetworkQuery
extends AlmanacQuery {
private $ids;
private $phids;
+ private $names;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function newResultObject() {
return new AlmanacNetwork();
}
+ public function withNames(array $names) {
+ $this->names = $names;
+ return $this;
+ }
+
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacNetworkNameNgrams(),
$ngrams);
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'network.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'network.phid IN (%Ls)',
$this->phids);
}
+ if ($this->names !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'network.name IN (%Ls)',
+ $this->names);
+ }
+
return $where;
}
protected function getPrimaryTableAlias() {
return 'network';
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}
diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php
index 4002651d0..5c7f65ddd 100644
--- a/src/applications/almanac/storage/AlmanacInterface.php
+++ b/src/applications/almanac/storage/AlmanacInterface.php
@@ -1,219 +1,223 @@
<?php
final class AlmanacInterface
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorConduitResultInterface {
protected $devicePHID;
protected $networkPHID;
protected $address;
protected $port;
private $device = self::ATTACHABLE;
private $network = self::ATTACHABLE;
public static function initializeNewInterface() {
return id(new AlmanacInterface());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'text64',
'port' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_location' => array(
'columns' => array('networkPHID', 'address', 'port'),
),
'key_device' => array(
'columns' => array('devicePHID'),
),
+ 'key_unique' => array(
+ 'columns' => array('devicePHID', 'networkPHID', 'address', 'port'),
+ 'unique' => true,
+ ),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
AlmanacInterfacePHIDType::TYPECONST);
}
public function getDevice() {
return $this->assertAttached($this->device);
}
public function attachDevice(AlmanacDevice $device) {
$this->device = $device;
return $this;
}
public function getNetwork() {
return $this->assertAttached($this->network);
}
public function attachNetwork(AlmanacNetwork $network) {
$this->network = $network;
return $this;
}
public function toAddress() {
return AlmanacAddress::newFromParts(
$this->getNetworkPHID(),
$this->getAddress(),
$this->getPort());
}
public function getAddressHash() {
return $this->toAddress()->toHash();
}
public function renderDisplayAddress() {
return $this->getAddress().':'.$this->getPort();
}
public function loadIsInUse() {
$binding = id(new AlmanacBindingQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withInterfacePHIDs(array($this->getPHID()))
->setLimit(1)
->executeOne();
return (bool)$binding;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getDevice()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getDevice()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
$notes = array(
pht('An interface inherits the policies of the device it belongs to.'),
pht(
'You must be able to view the network an interface resides on to '.
'view the interface.'),
);
return $notes;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getDevice()->isClusterDevice()) {
return array(
array(
new PhabricatorAlmanacApplication(),
AlmanacManageClusterServicesCapability::CAPABILITY,
),
);
}
break;
}
return array();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$bindings = id(new AlmanacBindingQuery())
->setViewer($engine->getViewer())
->withInterfacePHIDs(array($this->getPHID()))
->execute();
foreach ($bindings as $binding) {
$engine->destroyObject($binding);
}
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacInterfaceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacInterfaceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('devicePHID')
->setType('phid')
->setDescription(pht('The device the interface is on.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('networkPHID')
->setType('phid')
->setDescription(pht('The network the interface is part of.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('address')
->setType('string')
->setDescription(pht('The address of the interface.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('port')
->setType('int')
->setDescription(pht('The port number of the interface.')),
);
}
public function getFieldValuesForConduit() {
return array(
'devicePHID' => $this->getDevicePHID(),
'networkPHID' => $this->getNetworkPHID(),
'address' => (string)$this->getAddress(),
'port' => (int)$this->getPort(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php
index 6a530b4cb..6d2f23032 100644
--- a/src/applications/almanac/storage/AlmanacNetwork.php
+++ b/src/applications/almanac/storage/AlmanacNetwork.php
@@ -1,149 +1,156 @@
<?php
final class AlmanacNetwork
extends AlmanacDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface {
protected $name;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
public static function initializeNewNetwork() {
return id(new AlmanacNetwork())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
- 'name' => 'text128',
+ 'name' => 'sort128',
'mailKey' => 'bytes20',
+
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_name' => array(
+ 'columns' => array('name'),
+ 'unique' => true,
+ ),
),
) + 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/'.$this->getID().'/';
}
/* -( 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;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$interfaces = id(new AlmanacInterfaceQuery())
->setViewer($engine->getViewer())
->withNetworkPHIDs(array($this->getPHID()))
->execute();
foreach ($interfaces as $interface) {
$engine->destroyObject($interface);
}
$this->delete();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new AlmanacNetworkNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the network.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/almanac/util/AlmanacNames.php b/src/applications/almanac/util/AlmanacNames.php
index 68a387cb9..cee3c8115 100644
--- a/src/applications/almanac/util/AlmanacNames.php
+++ b/src/applications/almanac/util/AlmanacNames.php
@@ -1,63 +1,64 @@
<?php
final class AlmanacNames extends Phobject {
public static function validateName($name) {
if (strlen($name) < 3) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names must be '.
- 'at least 3 characters long.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'must be at least 3 characters long.'));
}
if (strlen($name) > 100) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names may not '.
- 'be more than 100 characters long.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'may not be more than 100 characters long.'));
}
if (!preg_match('/^[a-z0-9.-]+\z/', $name)) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names may only '.
- 'contain lowercase letters, numbers, hyphens, and periods.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'may only contain lowercase letters, numbers, hyphens, and '.
+ 'periods.'));
}
if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names may not '.
- 'have any segments containing only digits.'));
+ 'Almanac service, device, network, property and namespace names '.
+ 'may not have any segments containing only digits.'));
}
if (preg_match('/\.\./', $name)) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names may not '.
- 'contain multiple consecutive periods.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'may not contain multiple consecutive periods.'));
}
if (preg_match('/\\.-|-\\./', $name)) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names may not '.
- 'contain hyphens adjacent to periods.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'may not contain hyphens adjacent to periods.'));
}
if (preg_match('/--/', $name)) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names may not '.
- 'contain multiple consecutive hyphens.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'may not contain multiple consecutive hyphens.'));
}
if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) {
throw new Exception(
pht(
- 'Almanac service, device, property and namespace names must begin '.
- 'and end with a letter or number.'));
+ 'Almanac service, device, property, network and namespace names '.
+ 'must begin and end with a letter or number.'));
}
}
}
diff --git a/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php b/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php
index 140c1a966..f049587b2 100644
--- a/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php
+++ b/src/applications/almanac/xaction/AlmanacNetworkNameTransaction.php
@@ -1,44 +1,75 @@
<?php
final class AlmanacNetworkNameTransaction
extends AlmanacNetworkTransactionType {
const TRANSACTIONTYPE = 'almanac:network:name';
public function generateOldValue($object) {
return $object->getName();
}
public function applyInternalEffects($object, $value) {
$object->setName($value);
}
public function getTitle() {
return pht(
'%s renamed this network from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function getTitleForFeed() {
return pht(
'%s renamed %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
$errors[] = $this->newRequiredError(
pht('Network name is required.'));
}
+ foreach ($xactions as $xaction) {
+ $name = $xaction->getNewValue();
+
+ $message = null;
+ try {
+ AlmanacNames::validateName($name);
+ } catch (Exception $ex) {
+ $message = $ex->getMessage();
+ }
+
+ if ($message !== null) {
+ $errors[] = $this->newInvalidError($message, $xaction);
+ continue;
+ }
+
+ if ($name === $object->getName()) {
+ continue;
+ }
+
+ $other = id(new AlmanacNetworkQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withNames(array($name))
+ ->executeOne();
+ if ($other && ($other->getID() != $object->getID())) {
+ $errors[] = $this->newInvalidError(
+ pht('Almanac networks must have unique names.'),
+ $xaction);
+ continue;
+ }
+ }
+
return $errors;
}
}
diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
index 8522f0042..5bcb66c13 100644
--- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
+++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
@@ -1,571 +1,572 @@
<?php
abstract class PhabricatorAphlictManagementWorkflow
extends PhabricatorManagementWorkflow {
private $debug = false;
private $configData;
private $configPath;
final protected function setDebug($debug) {
$this->debug = $debug;
return $this;
}
protected function getLaunchArguments() {
return array(
array(
'name' => 'config',
'param' => 'file',
'help' => pht(
'Use a specific configuration file instead of the default '.
'configuration.'),
),
);
}
protected function parseLaunchArguments(PhutilArgumentParser $args) {
$config_file = $args->getArg('config');
if ($config_file) {
$full_path = Filesystem::resolvePath($config_file);
$show_path = $full_path;
} else {
$root = dirname(dirname(phutil_get_library_root('phabricator')));
$try = array(
'phabricator/conf/aphlict/aphlict.custom.json',
'phabricator/conf/aphlict/aphlict.default.json',
);
foreach ($try as $config) {
$full_path = $root.'/'.$config;
$show_path = $config;
if (Filesystem::pathExists($full_path)) {
break;
}
}
}
echo tsprintf(
"%s\n",
pht(
'Reading configuration from: %s',
$show_path));
try {
$data = Filesystem::readFile($full_path);
} catch (Exception $ex) {
throw new PhutilArgumentUsageException(
pht(
'Failed to read configuration file. %s',
$ex->getMessage()));
}
try {
$data = phutil_json_decode($data);
} catch (Exception $ex) {
throw new PhutilArgumentUsageException(
pht(
'Configuration file is not properly formatted JSON. %s',
$ex->getMessage()));
}
try {
PhutilTypeSpec::checkMap(
$data,
array(
'servers' => 'list<wild>',
'logs' => 'optional list<wild>',
'cluster' => 'optional list<wild>',
'pidfile' => 'string',
'memory.hint' => 'optional int',
));
} catch (Exception $ex) {
throw new PhutilArgumentUsageException(
pht(
'Configuration file has improper configuration keys at top '.
'level. %s',
$ex->getMessage()));
}
$servers = $data['servers'];
$has_client = false;
$has_admin = false;
$port_map = array();
foreach ($servers as $index => $server) {
PhutilTypeSpec::checkMap(
$server,
array(
'type' => 'string',
'port' => 'int',
'listen' => 'optional string|null',
'ssl.key' => 'optional string|null',
'ssl.cert' => 'optional string|null',
'ssl.chain' => 'optional string|null',
));
$port = $server['port'];
if (!isset($port_map[$port])) {
$port_map[$port] = $index;
} else {
throw new PhutilArgumentUsageException(
pht(
'Two servers (at indexes "%s" and "%s") both bind to the same '.
'port ("%s"). Each server must bind to a unique port.',
$port_map[$port],
$index,
$port));
}
$type = $server['type'];
switch ($type) {
case 'admin':
$has_admin = true;
break;
case 'client':
$has_client = true;
break;
default:
throw new PhutilArgumentUsageException(
pht(
'A specified server (at index "%s", on port "%s") has an '.
'invalid type ("%s"). Valid types are: admin, client.',
$index,
$port,
$type));
}
$ssl_key = idx($server, 'ssl.key');
$ssl_cert = idx($server, 'ssl.cert');
if (($ssl_key && !$ssl_cert) || ($ssl_cert && !$ssl_key)) {
throw new PhutilArgumentUsageException(
pht(
'A specified server (at index "%s", on port "%s") specifies '.
'only one of "%s" and "%s". Each server must specify neither '.
'(to disable SSL) or specify both (to enable it).',
$index,
$port,
'ssl.key',
'ssl.cert'));
}
$ssl_chain = idx($server, 'ssl.chain');
if ($ssl_chain && (!$ssl_key && !$ssl_cert)) {
throw new PhutilArgumentUsageException(
pht(
'A specified server (at index "%s", on port "%s") specifies '.
'a value for "%s", but no value for "%s" or "%s". Servers '.
'should only provide an SSL chain if they also provide an SSL '.
'key and SSL certificate.',
$index,
$port,
'ssl.chain',
'ssl.key',
'ssl.cert'));
}
}
if (!$servers) {
throw new PhutilArgumentUsageException(
pht(
'Configuration file does not specify any servers. This service '.
'will not be able to interact with the outside world if it does '.
'not listen on any ports. You must specify at least one "%s" '.
'server and at least one "%s" server.',
'admin',
'client'));
}
if (!$has_client) {
throw new PhutilArgumentUsageException(
pht(
'Configuration file does not specify any client servers. This '.
'service will be unable to transmit any notifications without a '.
'client server. You must specify at least one server with '.
'type "%s".',
'client'));
}
if (!$has_admin) {
throw new PhutilArgumentUsageException(
pht(
'Configuration file does not specify any administrative '.
'servers. This service will be unable to receive messages. '.
'You must specify at least one server with type "%s".',
'admin'));
}
$logs = idx($data, 'logs', array());
foreach ($logs as $index => $log) {
PhutilTypeSpec::checkMap(
$log,
array(
'path' => 'string',
));
$path = $log['path'];
try {
$dir = dirname($path);
if (!Filesystem::pathExists($dir)) {
Filesystem::createDirectory($dir, 0755, true);
}
} catch (FilesystemException $ex) {
throw new PhutilArgumentUsageException(
pht(
'Failed to create directory "%s" for specified log file (with '.
'index "%s"). You should manually create this directory or '.
'choose a different logfile location. %s',
$dir,
+ $index,
$ex->getMessage()));
}
}
$peer_map = array();
$cluster = idx($data, 'cluster', array());
foreach ($cluster as $index => $peer) {
PhutilTypeSpec::checkMap(
$peer,
array(
'host' => 'string',
'port' => 'int',
'protocol' => 'string',
));
$host = $peer['host'];
$port = $peer['port'];
$protocol = $peer['protocol'];
switch ($protocol) {
case 'http':
case 'https':
break;
default:
throw new PhutilArgumentUsageException(
pht(
'Configuration file specifies cluster peer ("%s", at index '.
'"%s") with an invalid protocol, "%s". Valid protocols are '.
'"%s" or "%s".',
$host,
$index,
$protocol,
'http',
'https'));
}
$peer_key = "{$host}:{$port}";
if (!isset($peer_map[$peer_key])) {
$peer_map[$peer_key] = $index;
} else {
throw new PhutilArgumentUsageException(
pht(
'Configuration file specifies cluster peer "%s" more than '.
'once (at indexes "%s" and "%s"). Each peer must have a '.
'unique host and port combination.',
$peer_key,
$peer_map[$peer_key],
$index));
}
}
$this->configData = $data;
$this->configPath = $full_path;
$pid_path = $this->getPIDPath();
try {
$dir = dirname($pid_path);
if (!Filesystem::pathExists($dir)) {
Filesystem::createDirectory($dir, 0755, true);
}
} catch (FilesystemException $ex) {
throw new PhutilArgumentUsageException(
pht(
'Failed to create directory "%s" for specified PID file. You '.
'should manually create this directory or choose a different '.
'PID file location. %s',
$dir,
$ex->getMessage()));
}
}
final public function getPIDPath() {
return $this->configData['pidfile'];
}
final public function getPID() {
$pid = null;
if (Filesystem::pathExists($this->getPIDPath())) {
$pid = (int)Filesystem::readFile($this->getPIDPath());
}
return $pid;
}
final public function cleanup($signo = null) {
global $g_future;
if ($g_future) {
$g_future->resolveKill();
$g_future = null;
}
Filesystem::remove($this->getPIDPath());
if ($signo !== null) {
$signame = phutil_get_signal_name($signo);
error_log("Caught signal {$signame}, exiting.");
}
exit(1);
}
public static function requireExtensions() {
self::mustHaveExtension('pcntl');
self::mustHaveExtension('posix');
}
private static function mustHaveExtension($ext) {
if (!extension_loaded($ext)) {
echo pht(
"ERROR: The PHP extension '%s' is not installed. You must ".
"install it to run Aphlict on this machine.",
$ext)."\n";
exit(1);
}
$extension = new ReflectionExtension($ext);
foreach ($extension->getFunctions() as $function) {
$function = $function->name;
if (!function_exists($function)) {
echo pht(
'ERROR: The PHP function %s is disabled. You must '.
'enable it to run Aphlict on this machine.',
$function.'()')."\n";
exit(1);
}
}
}
final protected function willLaunch() {
$console = PhutilConsole::getConsole();
$pid = $this->getPID();
if ($pid) {
throw new PhutilArgumentUsageException(
pht(
'Unable to start notifications server because it is already '.
'running. Use `%s` to restart it.',
'aphlict restart'));
}
if (posix_getuid() == 0) {
throw new PhutilArgumentUsageException(
pht('The notification server should not be run as root.'));
}
// Make sure we can write to the PID file.
if (!$this->debug) {
Filesystem::writeFile($this->getPIDPath(), '');
}
// First, start the server in configuration test mode with --test. This
// will let us error explicitly if there are missing modules, before we
// fork and lose access to the console.
$test_argv = $this->getServerArgv();
$test_argv[] = '--test=true';
execx('%C', $this->getStartCommand($test_argv));
}
private function getServerArgv() {
$server_argv = array();
$server_argv[] = '--config='.$this->configPath;
return $server_argv;
}
final protected function launch() {
$console = PhutilConsole::getConsole();
if ($this->debug) {
$console->writeOut(
"%s\n",
pht('Starting Aphlict server in foreground...'));
} else {
Filesystem::writeFile($this->getPIDPath(), getmypid());
}
$command = $this->getStartCommand($this->getServerArgv());
if (!$this->debug) {
declare(ticks = 1);
pcntl_signal(SIGINT, array($this, 'cleanup'));
pcntl_signal(SIGTERM, array($this, 'cleanup'));
}
register_shutdown_function(array($this, 'cleanup'));
if ($this->debug) {
$console->writeOut(
"%s\n\n $ %s\n\n",
pht('Launching server:'),
$command);
$err = phutil_passthru('%C', $command);
$console->writeOut(">>> %s\n", pht('Server exited!'));
exit($err);
} else {
while (true) {
global $g_future;
$g_future = new ExecFuture('exec %C', $command);
// Discard all output the subprocess produces: it writes to the log on
// disk, so we don't need to send the output anywhere and can just
// throw it away.
$g_future
->setStdoutSizeLimit(0)
->setStderrSizeLimit(0);
$g_future->resolve();
// If the server exited, wait a couple of seconds and restart it.
unset($g_future);
sleep(2);
}
}
}
/* -( Commands )----------------------------------------------------------- */
final protected function executeStartCommand() {
$console = PhutilConsole::getConsole();
$this->willLaunch();
$log = $this->getOverseerLogPath();
if ($log !== null) {
echo tsprintf(
"%s\n",
pht(
'Writing logs to: %s',
$log));
}
$pid = pcntl_fork();
if ($pid < 0) {
throw new Exception(
pht(
'Failed to %s!',
'fork()'));
} else if ($pid) {
$console->writeErr("%s\n", pht('Aphlict Server started.'));
exit(0);
}
// Redirect process errors to the error log. If we do not do this, any
// error the `aphlict` process itself encounters vanishes into thin air.
if ($log !== null) {
ini_set('error_log', $log);
}
// When we fork, the child process will inherit its parent's set of open
// file descriptors. If the parent process of bin/aphlict is waiting for
// bin/aphlict's file descriptors to close, it will be stuck waiting on
// the daemonized process. (This happens if e.g. bin/aphlict is started
// in another script using passthru().)
fclose(STDOUT);
fclose(STDERR);
$this->launch();
return 0;
}
final protected function executeStopCommand() {
$console = PhutilConsole::getConsole();
$pid = $this->getPID();
if (!$pid) {
$console->writeErr("%s\n", pht('Aphlict is not running.'));
return 0;
}
$console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid));
posix_kill($pid, SIGINT);
$start = time();
do {
if (!PhabricatorDaemonReference::isProcessRunning($pid)) {
$console->writeOut(
"%s\n",
pht('Aphlict Server (%s) exited normally.', $pid));
$pid = null;
break;
}
usleep(100000);
} while (time() < $start + 5);
if ($pid) {
$console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid));
posix_kill($pid, SIGKILL);
unset($pid);
}
Filesystem::remove($this->getPIDPath());
return 0;
}
private function getNodeBinary() {
if (Filesystem::binaryExists('nodejs')) {
return 'nodejs';
}
if (Filesystem::binaryExists('node')) {
return 'node';
}
throw new PhutilArgumentUsageException(
pht(
'No `%s` or `%s` binary was found in %s. You must install '.
'Node.js to start the Aphlict server.',
'nodejs',
'node',
'$PATH'));
}
private function getAphlictScriptPath() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/support/aphlict/server/aphlict_server.js';
}
private function getNodeArgv() {
$argv = array();
$hint = idx($this->configData, 'memory.hint');
$hint = nonempty($hint, 256);
$argv[] = sprintf('--max-old-space-size=%d', $hint);
return $argv;
}
private function getStartCommand(array $server_argv) {
return csprintf(
'%R %Ls -- %s %Ls',
$this->getNodeBinary(),
$this->getNodeArgv(),
$this->getAphlictScriptPath(),
$server_argv);
}
private function getOverseerLogPath() {
// For now, just return the first log. We could refine this eventually.
$logs = idx($this->configData, 'logs', array());
foreach ($logs as $log) {
return $log['path'];
}
return null;
}
}
diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
index 31bb2c22f..e559862c8 100644
--- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
@@ -1,92 +1,140 @@
<?php
final class PhabricatorAuditCommitStatusConstants extends Phobject {
+ private $key;
+ private $spec = array();
+
const NONE = 0;
const NEEDS_AUDIT = 1;
const CONCERN_RAISED = 2;
const PARTIALLY_AUDITED = 3;
const FULLY_AUDITED = 4;
const NEEDS_VERIFICATION = 5;
- public static function getStatusNameMap() {
- $map = array(
- self::NONE => pht('No Audits'),
- self::NEEDS_AUDIT => pht('Audit Required'),
- self::CONCERN_RAISED => pht('Concern Raised'),
- self::NEEDS_VERIFICATION => pht('Needs Verification'),
- self::PARTIALLY_AUDITED => pht('Partially Audited'),
- self::FULLY_AUDITED => pht('Audited'),
- );
+ const MODERN_NONE = 'none';
+ const MODERN_NEEDS_AUDIT = 'needs-audit';
+ const MODERN_CONCERN_RAISED = 'concern-raised';
+ const MODERN_PARTIALLY_AUDITED = 'partially-audited';
+ const MODERN_AUDITED = 'audited';
+ const MODERN_NEEDS_VERIFICATION = 'needs-verification';
+
+ public static function newForLegacyStatus($status) {
+ $map = self::getMap();
+
+ foreach ($map as $key => $spec) {
+ if (idx($spec, 'legacy') == $status) {
+ return self::newForStatus($key);
+ }
+ }
+
+ return self::newForStatus($status);
+ }
+
+ public static function newForStatus($status) {
+ $result = new self();
+
+ $result->key = $status;
+
+ $map = self::getMap();
+ if (isset($map[$status])) {
+ $result->spec = $map[$status];
+ }
+
+ return $result;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
- return $map;
+ public function getIcon() {
+ return idx($this->spec, 'icon');
+ }
+
+ public function getColor() {
+ return idx($this->spec, 'color');
+ }
+
+ public function getName() {
+ return idx($this->spec, 'name', pht('Unknown ("%s")', $this->key));
+ }
+
+ public static function getStatusNameMap() {
+ $map = self::getMap();
+ return ipull($map, 'name', 'legacy');
}
public static function getStatusName($code) {
return idx(self::getStatusNameMap(), $code, pht('Unknown'));
}
public static function getOpenStatusConstants() {
- return array(
- self::CONCERN_RAISED,
- self::NEEDS_AUDIT,
- self::NEEDS_VERIFICATION,
- self::PARTIALLY_AUDITED,
- );
+ $constants = array();
+ foreach (self::getMap() as $map) {
+ if (!$map['closed']) {
+ $constants[] = $map['legacy'];
+ }
+ }
+ return $constants;
}
public static function getStatusColor($code) {
- switch ($code) {
- case self::CONCERN_RAISED:
- $color = 'red';
- break;
- case self::NEEDS_AUDIT:
- $color = 'orange';
- break;
- case self::PARTIALLY_AUDITED:
- $color = 'yellow';
- break;
- case self::FULLY_AUDITED:
- $color = 'green';
- break;
- case self::NONE:
- $color = 'bluegrey';
- break;
- case self::NEEDS_VERIFICATION:
- $color = 'indigo';
- break;
- default:
- $color = null;
- break;
- }
- return $color;
+ $map = self::getMap();
+ $map = ipull($map, 'color', 'legacy');
+ return idx($map, $code);
}
public static function getStatusIcon($code) {
- switch ($code) {
- case self::CONCERN_RAISED:
- $icon = 'fa-times-circle';
- break;
- case self::NEEDS_AUDIT:
- $icon = 'fa-exclamation-circle';
- break;
- case self::PARTIALLY_AUDITED:
- $icon = 'fa-check-circle-o';
- break;
- case self::FULLY_AUDITED:
- $icon = 'fa-check-circle';
- break;
- case self::NONE:
- $icon = 'fa-check';
- break;
- case self::NEEDS_VERIFICATION:
- $icon = 'fa-refresh';
- break;
- default:
- $icon = null;
- break;
- }
- return $icon;
+ $map = self::getMap();
+ $map = ipull($map, 'icon', 'legacy');
+ return idx($map, $code);
}
+ private static function getMap() {
+ return array(
+ self::MODERN_NONE => array(
+ 'name' => pht('No Audits'),
+ 'legacy' => self::NONE,
+ 'icon' => 'fa-check',
+ 'color' => 'bluegrey',
+ 'closed' => true,
+ ),
+ self::MODERN_NEEDS_AUDIT => array(
+ 'name' => pht('Audit Required'),
+ 'legacy' => self::NEEDS_AUDIT,
+ 'icon' => 'fa-exclamation-circle',
+ 'color' => 'orange',
+ 'closed' => false,
+ ),
+ self::MODERN_CONCERN_RAISED => array(
+ 'name' => pht('Concern Raised'),
+ 'legacy' => self::CONCERN_RAISED,
+ 'icon' => 'fa-times-circle',
+ 'color' => 'red',
+ 'closed' => false,
+ ),
+ self::MODERN_PARTIALLY_AUDITED => array(
+ 'name' => pht('Partially Audited'),
+ 'legacy' => self::PARTIALLY_AUDITED,
+ 'icon' => 'fa-check-circle-o',
+ 'color' => 'yellow',
+ 'closed' => false,
+ ),
+ self::MODERN_AUDITED => array(
+ 'name' => pht('Audited'),
+ 'legacy' => self::FULLY_AUDITED,
+ 'icon' => 'fa-check-circle',
+ 'color' => 'green',
+ 'closed' => true,
+ ),
+ self::MODERN_NEEDS_VERIFICATION => array(
+ 'name' => pht('Needs Verification'),
+ 'legacy' => self::NEEDS_VERIFICATION,
+ 'icon' => 'fa-refresh',
+ 'color' => 'indigo',
+ 'closed' => false,
+ ),
+ );
+ }
}
diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index 984e2c147..673abeb85 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -1,883 +1,895 @@
<?php
final class PhabricatorAuditEditor
extends PhabricatorApplicationTransactionEditor {
const MAX_FILES_SHOWN_IN_EMAIL = 1000;
private $affectedFiles;
private $rawPatch;
private $auditorPHIDs = array();
private $didExpandInlineState = false;
private $oldAuditStatus = null;
public function setRawPatch($patch) {
$this->rawPatch = $patch;
return $this;
}
public function getRawPatch() {
return $this->rawPatch;
}
public function getEditorApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getEditorObjectsDescription() {
return pht('Audits');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
$types[] = PhabricatorAuditTransaction::TYPE_COMMIT;
// TODO: These will get modernized eventually, but that can happen one
// at a time later on.
$types[] = PhabricatorAuditActionConstants::INLINE;
return $types;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$this->didExpandInlineState = true;
break;
}
}
$this->oldAuditStatus = $object->getAuditStatus();
$object->loadAndAttachAuditAuthority(
$this->getActor(),
$this->getActingAsPHID());
return parent::expandTransactions($object, $xactions);
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
return $xaction->hasComment();
}
return parent::transactionHasEffect($object, $xaction);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
return;
case PhabricatorAuditActionConstants::INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new PhabricatorAuditTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load auditors explicitly; we may not have them if the caller was a
// generic piece of infrastructure.
$commit = id(new DiffusionCommitQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getID()))
->needAuditRequests(true)
->executeOne();
if (!$commit) {
throw new Exception(
pht('Failed to load commit during transaction finalization!'));
}
$object->attachAudits($commit->getAudits());
$status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
$status_closed = PhabricatorAuditStatusConstants::CLOSED;
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
$status_accepted = PhabricatorAuditStatusConstants::ACCEPTED;
$status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($object->getAuthorPHID()) &&
($actor_phid == $object->getAuthorPHID());
$import_status_flag = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD;
break;
}
}
$old_status = $this->oldAuditStatus;
$requests = $object->getAudits();
$object->updateAuditStatus($requests);
$new_status = $object->getAuditStatus();
$object->save();
if ($import_status_flag) {
$object->writeImportStatusFlag($import_status_flag);
}
$partial_status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED;
// If the commit has changed state after this edit, add an informational
// transaction about the state change.
if ($old_status != $new_status) {
if ($new_status == $partial_status) {
// This state isn't interesting enough to get a transaction. The
// best way we could lead the user forward is something like "This
// commit still requires additional audits." but that's redundant and
// probably not very useful.
} else {
$xaction = $object->getApplicationTransactionTemplate()
->setTransactionType(DiffusionCommitStateTransaction::TRANSACTIONTYPE)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($object, $xaction);
$xaction->save();
}
}
// Collect auditor PHIDs for building mail.
$this->auditorPHIDs = mpull($object->getAudits(), 'getAuditorPHID');
return $xactions;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$request = $this->createAuditRequestTransactionFromCommitMessage(
$object);
if ($request) {
$xactions[] = $request;
$this->setUnmentionablePHIDMap($request->getNewValue());
}
break;
default:
break;
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
- $actor_is_author = ($object->getAuthorPHID() == $actor_phid);
- if (!$actor_is_author) {
- break;
- }
+ $author_phid = $object->getAuthorPHID();
+ $actor_is_author = ($actor_phid == $author_phid);
$state_map = PhabricatorTransactions::getInlineStateMap();
- $inlines = id(new DiffusionDiffInlineCommentQuery())
+ $query = id(new DiffusionDiffInlineCommentQuery())
->setViewer($this->getActor())
->withCommitPHIDs(array($object->getPHID()))
- ->withFixedStates(array_keys($state_map))
+ ->withFixedStates(array_keys($state_map));
+
+ $inlines = array();
+
+ $inlines[] = id(clone $query)
+ ->withAuthorPHIDs(array($actor_phid))
+ ->withHasTransaction(false)
->execute();
+ if ($actor_is_author) {
+ $inlines[] = id(clone $query)
+ ->withHasTransaction(true)
+ ->execute();
+ }
+
+ $inlines = array_mergev($inlines);
+
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $xactions;
}
private function createAuditRequestTransactionFromCommitMessage(
PhabricatorRepositoryCommit $commit) {
$actor = $this->getActor();
$data = $commit->getCommitData();
$message = $data->getCommitMessage();
$result = DifferentialCommitMessageParser::newStandardParser($actor)
->setRaiseMissingFieldErrors(false)
->parseFields($message);
$field_key = DifferentialAuditorsCommitMessageField::FIELDKEY;
$phids = idx($result, $field_key, null);
if (!$phids) {
return array();
}
// If a commit lists its author as an auditor, just pretend it does not.
foreach ($phids as $key => $phid) {
if ($phid == $commit->getAuthorPHID()) {
unset($phids[$key]);
}
}
if (!$phids) {
return array();
}
return $commit->getApplicationTransactionTemplate()
->setTransactionType(DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE)
->setNewValue(
array(
'+' => array_fuse($phids),
));
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorAuditActionConstants::INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function supportsSearch() {
return true;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
$actor = $this->getActor();
$result = array();
// Some interactions (like "Fixes Txxx" interacting with Maniphest) have
// already been processed, so we're only re-parsing them here to avoid
// generating an extra redundant mention. Other interactions are being
// processed for the first time.
// We're only recognizing magic in the commit message itself, not in
// audit comments.
$is_commit = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$is_commit = true;
break;
}
}
if (!$is_commit) {
return $result;
}
$flat_blocks = mpull($changes, 'getNewValue');
$huge_block = implode("\n\n", $flat_blocks);
$phid_map = array();
$phid_map[] = $this->getUnmentionablePHIDMap();
$monograms = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($huge_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$monograms[] = $monogram;
}
}
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($huge_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$monograms[] = $monogram;
}
}
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withNames($monograms)
->execute();
$phid_map[] = mpull($objects, 'getPHID', 'getPHID');
$reverts_refs = id(new DifferentialCustomFieldRevertsParser())
->parseCorpus($huge_block);
$reverts = array_mergev(ipull($reverts_refs, 'monograms'));
if ($reverts) {
// Only allow commits to revert other commits in the same repository.
$reverted_commits = id(new DiffusionCommitQuery())
->setViewer($actor)
->withRepository($object->getRepository())
->withIdentifiers($reverts)
->execute();
$reverted_revisions = id(new PhabricatorObjectQuery())
->setViewer($actor)
->withNames($reverts)
->withTypes(
array(
DifferentialRevisionPHIDType::TYPECONST,
))
->execute();
$reverted_phids =
mpull($reverted_commits, 'getPHID', 'getPHID') +
mpull($reverted_revisions, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a commit
// reverts itself, although this would be exceptionally clever in Git
// or Mercurial.
unset($reverted_phids[$object->getPHID()]);
$reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
$result[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $reverts_edge)
->setNewValue(array('+' => $reverted_phids));
$phid_map[] = $reverted_phids;
}
$phid_map = array_mergev($phid_map);
$this->setUnmentionablePHIDMap($phid_map);
return $result;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
$reply_handler = new PhabricatorAuditReplyHandler();
$reply_handler->setMailReceiver($object);
return $reply_handler;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// For backward compatibility, use this legacy thread ID.
return 'diffusion-audit-'.$object->getPHID();
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$identifier = $object->getCommitIdentifier();
$repository = $object->getRepository();
$summary = $object->getSummary();
$name = $repository->formatCommitName($identifier);
$subject = "{$name}: {$summary}";
$template = id(new PhabricatorMetaMTAMail())
->setSubject($subject);
$this->attachPatch(
$template,
$object);
return $template;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$this->requireAuditors($object);
$phids = array();
if ($object->getAuthorPHID()) {
$phids[] = $object->getAuthorPHID();
}
foreach ($object->getAudits() as $audit) {
if (!$audit->isInteresting()) {
// Don't send mail to uninteresting auditors, like packages which
// own this code but which audits have not triggered for.
continue;
}
if (!$audit->isResigned()) {
$phids[] = $audit->getAuditorPHID();
}
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
$this->requireAuditors($object);
$phids = array();
foreach ($object->getAudits() as $auditor) {
if ($auditor->isResigned()) {
$phids[] = $auditor->getAuditorPHID();
}
}
return $phids;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$type_inline = PhabricatorAuditActionConstants::INLINE;
$type_push = PhabricatorAuditTransaction::TYPE_COMMIT;
$is_commit = false;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
if ($xaction->getTransactionType() == $type_push) {
$is_commit = true;
}
}
if ($inlines) {
$body->addTextSection(
pht('INLINE COMMENTS'),
$this->renderInlineCommentsForMail($object, $inlines));
}
if ($is_commit) {
$data = $object->getCommitData();
$body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles);
$this->inlinePatch(
$body,
$object);
}
$data = $object->getCommitData();
$user_phids = array();
$author_phid = $object->getAuthorPHID();
if ($author_phid) {
$user_phids[$author_phid][] = pht('Author');
}
$committer_phid = $data->getCommitDetail('committerPHID');
if ($committer_phid && ($committer_phid != $author_phid)) {
$user_phids[$committer_phid][] = pht('Committer');
}
foreach ($this->auditorPHIDs as $auditor_phid) {
$user_phids[$auditor_phid][] = pht('Auditor');
}
// TODO: It would be nice to show pusher here too, but that information
// is a little tricky to get at right now.
if ($user_phids) {
$handle_phids = array_keys($user_phids);
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($handle_phids)
->execute();
$user_info = array();
foreach ($user_phids as $phid => $roles) {
$user_info[] = pht(
'%s (%s)',
$handles[$phid]->getName(),
implode(', ', $roles));
}
$body->addTextSection(
pht('USERS'),
implode("\n", $user_info));
}
$monogram = $object->getRepository()->formatCommitName(
$object->getCommitIdentifier());
$body->addLinkSection(
pht('COMMIT'),
PhabricatorEnv::getProductionURI('/'.$monogram));
return $body;
}
private function attachPatch(
PhabricatorMetaMTAMail $template,
PhabricatorRepositoryCommit $commit) {
if (!$this->getRawPatch()) {
return;
}
$attach_key = 'metamta.diffusion.attach-patches';
$attach_patches = PhabricatorEnv::getEnvConfig($attach_key);
if (!$attach_patches) {
return;
}
$repository = $commit->getRepository();
$encoding = $repository->getDetail('encoding', 'UTF-8');
$raw_patch = $this->getRawPatch();
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$template->addAttachment(
new PhabricatorMetaMTAAttachment(
$raw_patch,
$commit_name.'.patch',
'text/x-patch; charset='.$encoding));
}
private function inlinePatch(
PhabricatorMetaMTAMailBody $body,
PhabricatorRepositoryCommit $commit) {
if (!$this->getRawPatch()) {
return;
}
$inline_key = 'metamta.diffusion.inline-patches';
$inline_patches = PhabricatorEnv::getEnvConfig($inline_key);
if (!$inline_patches) {
return;
}
$repository = $commit->getRepository();
$raw_patch = $this->getRawPatch();
$result = null;
$len = substr_count($raw_patch, "\n");
if ($len <= $inline_patches) {
// We send email as utf8, so we need to convert the text to utf8 if
// we can.
$encoding = $repository->getDetail('encoding', 'UTF-8');
if ($encoding) {
$raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);
}
$result = phutil_utf8ize($raw_patch);
}
if ($result) {
$result = "PATCH\n\n{$result}\n";
}
$body->addRawSection($result);
}
private function renderInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inline_xactions) {
$inlines = mpull($inline_xactions, 'getComment');
$block = array();
$path_map = id(new DiffusionPathQuery())
->withPathIDs(mpull($inlines, 'getPathID'))
->execute();
$path_map = ipull($path_map, 'path', 'id');
foreach ($inlines as $inline) {
$path = idx($path_map, $inline->getPathID());
if ($path === null) {
continue;
}
$start = $inline->getLineNumber();
$len = $inline->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$content = $inline->getContent();
$block[] = "{$path}:{$range} {$content}";
}
return implode("\n", $block);
}
public function getMailTagsMap() {
return array(
PhabricatorAuditTransaction::MAILTAG_COMMIT =>
pht('A commit is created.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN =>
pht('A commit has a concerned raised against it.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT =>
pht('A commit is accepted.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN =>
pht('A commit has an auditor resign.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE =>
pht('A commit is closed.'),
PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS =>
pht('A commit has auditors added.'),
PhabricatorAuditTransaction::MAILTAG_ADD_CCS =>
pht("A commit's subscribers change."),
PhabricatorAuditTransaction::MAILTAG_PROJECTS =>
pht("A commit's projects change."),
PhabricatorAuditTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a commit.'),
PhabricatorAuditTransaction::MAILTAG_OTHER =>
pht('Other commit activity not listed above occurs.'),
);
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$repository = $object->getRepository();
if (!$repository->shouldPublish()) {
return false;
}
return true;
default:
break;
}
}
return parent::shouldApplyHeraldRules($object, $xactions);
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldCommitAdapter())
->setObject($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$limit = self::MAX_FILES_SHOWN_IN_EMAIL;
$files = $adapter->loadAffectedPaths();
sort($files);
if (count($files) > $limit) {
array_splice($files, $limit);
$files[] = pht(
'(This commit affected more than %d files. Only %d are shown here '.
'and additional ones are truncated.)',
$limit,
$limit);
}
$this->affectedFiles = implode("\n", $files);
return array();
}
private function isCommitMostlyImported(PhabricatorLiskDAO $object) {
$has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE;
$has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE;
// Don't publish feed stories or email about events which occur during
// import. In particular, this affects tasks being attached when they are
// closed by "Fixes Txxxx" in a commit message. See T5851.
$mask = ($has_message | $has_changes);
return $object->isPartiallyImported($mask);
}
private function shouldPublishRepositoryActivity(
PhabricatorLiskDAO $object,
array $xactions) {
// not every code path loads the repository so tread carefully
// TODO: They should, and then we should simplify this.
$repository = $object->getRepository($assert_attached = false);
if ($repository != PhabricatorLiskDAO::ATTACHABLE) {
if (!$repository->shouldPublish()) {
return false;
}
}
return $this->isCommitMostlyImported($object);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function getCustomWorkerState() {
return array(
'rawPatch' => $this->rawPatch,
'affectedFiles' => $this->affectedFiles,
'auditorPHIDs' => $this->auditorPHIDs,
);
}
protected function getCustomWorkerStateEncoding() {
return array(
'rawPatch' => self::STORAGE_ENCODING_BINARY,
);
}
protected function loadCustomWorkerState(array $state) {
$this->rawPatch = idx($state, 'rawPatch');
$this->affectedFiles = idx($state, 'affectedFiles');
$this->auditorPHIDs = idx($state, 'auditorPHIDs');
return $this;
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
return id(new DiffusionCommitQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getID()))
->needAuditRequests(true)
->needCommitData(true)
->executeOne();
}
private function requireAuditors(PhabricatorRepositoryCommit $commit) {
if ($commit->hasAttachedAudits()) {
return;
}
$with_auditors = id(new DiffusionCommitQuery())
->setViewer($this->getActor())
->needAuditRequests(true)
->withPHIDs(array($commit->getPHID()))
->executeOne();
if (!$with_auditors) {
throw new Exception(
pht(
'Failed to reload commit ("%s").',
$commit->getPHID()));
}
$commit->attachAudits($with_auditors->getAudits());
}
}
diff --git a/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php b/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php
index db6ce096c..96d06e65c 100644
--- a/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php
+++ b/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php
@@ -1,60 +1,58 @@
<?php
final class PhabricatorAuditSynchronizeManagementWorkflow
extends PhabricatorAuditManagementWorkflow {
protected function didConstruct() {
$this
->setName('synchronize')
->setExamples('**synchronize** ...')
->setSynopsis(pht('Update audit status for commits.'))
->setArguments(
array_merge(
$this->getCommitConstraintArguments(),
array()));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$objects = $this->loadCommitsWithConstraints($args);
foreach ($objects as $object) {
$commits = $this->loadCommitsForConstraintObject($object);
foreach ($commits as $commit) {
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($commit->getPHID()))
->needAuditRequests(true)
->executeOne();
if (!$commit) {
continue;
}
- $old_status = $commit->getAuditStatus();
+ $old_status = $commit->getAuditStatusObject();
$commit->updateAuditStatus($commit->getAudits());
- $new_status = $commit->getAuditStatus();
+ $new_status = $commit->getAuditStatusObject();
- if ($old_status == $new_status) {
+ if ($old_status->getKey() == $new_status->getKey()) {
echo tsprintf(
"%s\n",
pht(
'No changes for "%s".',
$commit->getDisplayName()));
} else {
echo tsprintf(
"%s\n",
pht(
'Updating "%s": "%s" -> "%s".',
$commit->getDisplayName(),
- PhabricatorAuditCommitStatusConstants::getStatusName(
- $old_status),
- PhabricatorAuditCommitStatusConstants::getStatusName(
- $new_status)));
+ $old_status->getName(),
+ $new_status->getName()));
$commit->save();
}
}
}
}
}
diff --git a/src/applications/audit/query/PhabricatorCommitSearchEngine.php b/src/applications/audit/query/PhabricatorCommitSearchEngine.php
index 6811427c9..e1b294a9d 100644
--- a/src/applications/audit/query/PhabricatorCommitSearchEngine.php
+++ b/src/applications/audit/query/PhabricatorCommitSearchEngine.php
@@ -1,228 +1,252 @@
<?php
final class PhabricatorCommitSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Diffusion Commits');
}
public function getApplicationClassName() {
return 'PhabricatorDiffusionApplication';
}
public function newQuery() {
return id(new DiffusionCommitQuery())
->needAuditRequests(true)
->needCommitData(true)
->needDrafts(true);
}
protected function newResultBuckets() {
return DiffusionCommitResultBucket::getAllResultBuckets();
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['responsiblePHIDs']) {
$query->withResponsiblePHIDs($map['responsiblePHIDs']);
}
if ($map['auditorPHIDs']) {
$query->withAuditorPHIDs($map['auditorPHIDs']);
}
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);
}
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
if ($map['repositoryPHIDs']) {
$query->withRepositoryPHIDs($map['repositoryPHIDs']);
}
if ($map['packagePHIDs']) {
$query->withPackagePHIDs($map['packagePHIDs']);
}
if ($map['unreachable'] !== null) {
$query->withUnreachable($map['unreachable']);
}
+ if ($map['ancestorsOf']) {
+ $query->withAncestorsOf($map['ancestorsOf']);
+ }
+
return $query;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Responsible Users'))
->setKey('responsiblePHIDs')
->setConduitKey('responsible')
->setAliases(array('responsible', 'responsibles', 'responsiblePHID'))
- ->setDatasource(new DifferentialResponsibleDatasource()),
+ ->setDatasource(new DifferentialResponsibleDatasource())
+ ->setDescription(
+ pht(
+ 'Find commits where given users, projects, or packages are '.
+ 'responsible for the next steps in the audit workflow.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setConduitKey('authors')
- ->setAliases(array('author', 'authors', 'authorPHID')),
+ ->setAliases(array('author', 'authors', 'authorPHID'))
+ ->setDescription(pht('Find commits authored by particular users.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Auditors'))
->setKey('auditorPHIDs')
->setConduitKey('auditors')
->setAliases(array('auditor', 'auditors', 'auditorPHID'))
- ->setDatasource(new DiffusionAuditorFunctionDatasource()),
+ ->setDatasource(new DiffusionAuditorFunctionDatasource())
+ ->setDescription(
+ pht(
+ 'Find commits where given users, projects, or packages are '.
+ 'auditors.')),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Audit Status'))
->setKey('statuses')
->setAliases(array('status'))
- ->setOptions(PhabricatorAuditCommitStatusConstants::getStatusNameMap()),
+ ->setOptions(PhabricatorAuditCommitStatusConstants::getStatusNameMap())
+ ->setDescription(pht('Find commits with given audit statuses.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Repositories'))
->setKey('repositoryPHIDs')
->setConduitKey('repositories')
->setAliases(array('repository', 'repositories', 'repositoryPHID'))
- ->setDatasource(new DiffusionRepositoryFunctionDatasource()),
+ ->setDatasource(new DiffusionRepositoryFunctionDatasource())
+ ->setDescription(pht('Find commits in particular repositories.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Packages'))
->setKey('packagePHIDs')
->setConduitKey('packages')
->setAliases(array('package', 'packages', 'packagePHID'))
- ->setDatasource(new PhabricatorOwnersPackageDatasource()),
+ ->setDatasource(new PhabricatorOwnersPackageDatasource())
+ ->setDescription(
+ pht('Find commits which affect given packages.')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Unreachable'))
->setKey('unreachable')
->setOptions(
pht('(Show All)'),
pht('Show Only Unreachable Commits'),
pht('Hide Unreachable Commits'))
->setDescription(
pht(
'Find or exclude unreachable commits which are not ancestors of '.
'any branch, tag, or ref.')),
+ id(new PhabricatorSearchStringListField())
+ ->setLabel(pht('Ancestors Of'))
+ ->setKey('ancestorsOf')
+ ->setDescription(
+ pht(
+ 'Find commits which are ancestors of a particular ref, '.
+ 'like "master".')),
);
}
protected function getURI($path) {
return '/diffusion/commit/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['active'] = pht('Active Audits');
$names['authored'] = pht('Authored');
$names['audited'] = pht('Audited');
}
$names['all'] = pht('All Commits');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer = $this->requireViewer();
$viewer_phid = $viewer->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'active':
$bucket_key = DiffusionCommitRequiredActionResultBucket::BUCKETKEY;
$open = PhabricatorAuditCommitStatusConstants::getOpenStatusConstants();
$query
->setParameter('responsiblePHIDs', array($viewer_phid))
->setParameter('statuses', $open)
->setParameter('bucket', $bucket_key)
->setParameter('unreachable', false);
return $query;
case 'authored':
$query
->setParameter('authorPHIDs', array($viewer_phid));
return $query;
case 'audited':
$query
->setParameter('auditorPHIDs', array($viewer_phid));
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $commits,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$viewer = $this->requireViewer();
$bucket = $this->getResultBucket($query);
$template = id(new PhabricatorAuditListView())
->setViewer($viewer)
->setShowDrafts(true);
$views = array();
if ($bucket) {
$bucket->setViewer($viewer);
try {
$groups = $bucket->newResultGroups($query, $commits);
foreach ($groups as $group) {
// Don't show groups in Dashboard Panels
if ($group->getObjects() || !$this->isPanelContext()) {
$views[] = id(clone $template)
->setHeader($group->getName())
->setNoDataString($group->getNoDataString())
->setCommits($group->getObjects());
}
}
} catch (Exception $ex) {
$this->addError($ex->getMessage());
}
} else {
$views[] = id(clone $template)
->setCommits($commits)
->setNoDataString(pht('No commits found.'));
}
if (!$views) {
$views[] = id(new PhabricatorAuditListView())
->setViewer($viewer)
->setNoDataString(pht('No commits found.'));
}
if (count($views) == 1) {
$list = head($views)->buildList();
} else {
$list = $views;
}
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($list);
return $result;
}
protected function getNewUserBody() {
$view = id(new PHUIBigInfoView())
->setIcon('fa-check-circle-o')
->setTitle(pht('Welcome to Audit'))
->setDescription(
pht('Post-commit code review and auditing. Audits you are assigned '.
'to will appear here.'));
return $view;
}
}
diff --git a/src/applications/audit/view/PhabricatorAuditListView.php b/src/applications/audit/view/PhabricatorAuditListView.php
index cb9fecac3..fb56e7cd5 100644
--- a/src/applications/audit/view/PhabricatorAuditListView.php
+++ b/src/applications/audit/view/PhabricatorAuditListView.php
@@ -1,182 +1,179 @@
<?php
final class PhabricatorAuditListView extends AphrontView {
private $commits = array();
private $header;
private $showDrafts;
private $noDataString;
private $highlightedAudits;
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function getNoDataString() {
return $this->noDataString;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function getHeader() {
return $this->header;
}
public function setShowDrafts($show_drafts) {
$this->showDrafts = $show_drafts;
return $this;
}
public function getShowDrafts() {
return $this->showDrafts;
}
/**
* These commits should have both commit data and audit requests attached.
*/
public function setCommits(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commits = mpull($commits, null, 'getPHID');
return $this;
}
public function getCommits() {
return $this->commits;
}
private function getCommitDescription($phid) {
if ($this->commits === null) {
return pht('(Unknown Commit)');
}
$commit = idx($this->commits, $phid);
if (!$commit) {
return pht('(Unknown Commit)');
}
$summary = $commit->getCommitData()->getSummary();
if (strlen($summary)) {
return $summary;
}
// No summary, so either this is still importing or just has an empty
// commit message.
if (!$commit->isImported()) {
return pht('(Importing Commit...)');
} else {
return pht('(Untitled Commit)');
}
}
public function render() {
$list = $this->buildList();
$list->setFlush(true);
return $list->render();
}
public function buildList() {
$viewer = $this->getViewer();
$rowc = array();
$phids = array();
foreach ($this->getCommits() as $commit) {
$phids[] = $commit->getPHID();
foreach ($commit->getAudits() as $audit) {
$phids[] = $audit->getAuditorPHID();
}
$author_phid = $commit->getAuthorPHID();
if ($author_phid) {
$phids[] = $author_phid;
}
}
$handles = $viewer->loadHandles($phids);
$show_drafts = $this->getShowDrafts();
$draft_icon = id(new PHUIIconView())
->setIcon('fa-comment yellow')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Unsubmitted Comments'),
));
$list = new PHUIObjectItemListView();
foreach ($this->commits as $commit) {
$commit_phid = $commit->getPHID();
$commit_handle = $handles[$commit_phid];
$committed = null;
$commit_name = $commit_handle->getName();
$commit_link = $commit_handle->getURI();
$commit_desc = $this->getCommitDescription($commit_phid);
$committed = phabricator_datetime($commit->getEpoch(), $viewer);
- $status = $commit->getAuditStatus();
-
- $status_text =
- PhabricatorAuditCommitStatusConstants::getStatusName($status);
- $status_color =
- PhabricatorAuditCommitStatusConstants::getStatusColor($status);
- $status_icon =
- PhabricatorAuditCommitStatusConstants::getStatusIcon($status);
+ $status = $commit->getAuditStatusObject();
+
+ $status_text = $status->getName();
+ $status_color = $status->getColor();
+ $status_icon = $status->getIcon();
$author_phid = $commit->getAuthorPHID();
if ($author_phid) {
$author_name = $handles[$author_phid]->renderLink();
} else {
$author_name = $commit->getCommitData()->getAuthorName();
}
$item = id(new PHUIObjectItemView())
->setObjectName($commit_name)
->setHeader($commit_desc)
->setHref($commit_link)
->setDisabled($commit->isUnreachable())
->addByline(pht('Author: %s', $author_name))
->addIcon('none', $committed);
if ($show_drafts) {
if ($commit->getHasDraft($viewer)) {
$item->addAttribute($draft_icon);
}
}
$audits = $commit->getAudits();
$auditor_phids = mpull($audits, 'getAuditorPHID');
if ($auditor_phids) {
$auditor_list = $handles->newSublist($auditor_phids)
->renderList()
->setAsInline(true);
} else {
$auditor_list = phutil_tag('em', array(), pht('None'));
}
$item->addAttribute(pht('Auditors: %s', $auditor_list));
if ($status_color) {
$item->setStatusIcon($status_icon.' '.$status_color, $status_text);
}
$list->addItem($item);
}
if ($this->noDataString) {
$list->setNoDataString($this->noDataString);
}
if ($this->header) {
$list->setHeader($this->header);
}
return $list;
}
}
diff --git a/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php b/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php
index 77b666ea4..d8474085b 100644
--- a/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php
+++ b/src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php
@@ -1,138 +1,138 @@
<?php
final class PhabricatorAuthSSHKeyQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
- const AUTHFILE_CACHEKEY = 'ssh.authfile';
+ const AUTHSTRUCT_CACHEKEY = 'ssh.authstruct';
private $ids;
private $phids;
private $objectPHIDs;
private $keys;
private $isActive;
public static function deleteSSHKeyCache() {
$cache = PhabricatorCaches::getMutableCache();
- $authfile_key = self::AUTHFILE_CACHEKEY;
+ $authfile_key = self::AUTHSTRUCT_CACHEKEY;
$cache->deleteKey($authfile_key);
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withKeys(array $keys) {
assert_instances_of($keys, 'PhabricatorAuthSSHPublicKey');
$this->keys = $keys;
return $this;
}
public function withIsActive($active) {
$this->isActive = $active;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthSSHKey();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $keys) {
$object_phids = mpull($keys, 'getObjectPHID');
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
foreach ($keys as $key => $ssh_key) {
$object = idx($objects, $ssh_key->getObjectPHID());
// We must have an object, and that object must be a valid object for
// SSH keys.
if (!$object || !($object instanceof PhabricatorSSHPublicKeyInterface)) {
$this->didRejectResult($ssh_key);
unset($keys[$key]);
continue;
}
$ssh_key->attachObject($object);
}
return $keys;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->keys !== null) {
$sql = array();
foreach ($this->keys as $key) {
$sql[] = qsprintf(
$conn,
'(keyType = %s AND keyIndex = %s)',
$key->getType(),
$key->getHash());
}
$where[] = implode(' OR ', $sql);
}
if ($this->isActive !== null) {
if ($this->isActive) {
$where[] = qsprintf(
$conn,
'isActive = %d',
1);
} else {
$where[] = qsprintf(
$conn,
'isActive IS NULL');
}
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php
index 5343e622f..3bcb95693 100644
--- a/src/applications/auth/storage/PhabricatorAuthPassword.php
+++ b/src/applications/auth/storage/PhabricatorAuthPassword.php
@@ -1,235 +1,236 @@
<?php
final class PhabricatorAuthPassword
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $objectPHID;
protected $passwordType;
protected $passwordHash;
protected $passwordSalt;
protected $isRevoked;
protected $legacyDigestFormat;
private $object = self::ATTACHABLE;
const PASSWORD_TYPE_ACCOUNT = 'account';
const PASSWORD_TYPE_VCS = 'vcs';
const PASSWORD_TYPE_TEST = 'test';
public static function initializeNewPassword(
PhabricatorAuthPasswordHashInterface $object,
$type) {
return id(new self())
->setObjectPHID($object->getPHID())
->attachObject($object)
->setPasswordType($type)
->setIsRevoked(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'passwordType' => 'text64',
'passwordHash' => 'text128',
'passwordSalt' => 'text64',
'isRevoked' => 'bool',
'legacyDigestFormat' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_role' => array(
'columns' => array('objectPHID', 'passwordType'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorAuthPasswordPHIDType::TYPECONST;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getHasher() {
$hash = $this->newPasswordEnvelope();
return PhabricatorPasswordHasher::getHasherForHash($hash);
}
public function canUpgrade() {
// If this password uses a legacy digest format, we can upgrade it to the
// new digest format even if a better hasher isn't available.
if ($this->getLegacyDigestFormat() !== null) {
return true;
}
$hash = $this->newPasswordEnvelope();
return PhabricatorPasswordHasher::canUpgradeHash($hash);
}
public function upgradePasswordHasher(
PhutilOpaqueEnvelope $envelope,
PhabricatorAuthPasswordHashInterface $object) {
// Before we make changes, double check that this is really the correct
// password. It could be really bad if we "upgraded" a password and changed
// the secret!
if (!$this->comparePassword($envelope, $object)) {
throw new Exception(
pht(
'Attempting to upgrade password hasher, but the password for the '.
'upgrade is not the stored credential!'));
}
return $this->setPassword($envelope, $object);
}
public function setPassword(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
return $this->setPasswordWithHasher($password, $object, $hasher);
}
public function setPasswordWithHasher(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object,
PhabricatorPasswordHasher $hasher) {
if (!strlen($password->openEnvelope())) {
throw new Exception(
pht('Attempting to set an empty password!'));
}
// Generate (or regenerate) the salt first.
$new_salt = Filesystem::readRandomCharacters(64);
$this->setPasswordSalt($new_salt);
// Clear any legacy digest format to force a modern digest.
$this->setLegacyDigestFormat(null);
$digest = $this->digestPassword($password, $object);
$hash = $hasher->getPasswordHashForStorage($digest);
$raw_hash = $hash->openEnvelope();
return $this->setPasswordHash($raw_hash);
}
public function comparePassword(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object) {
$digest = $this->digestPassword($password, $object);
$hash = $this->newPasswordEnvelope();
return PhabricatorPasswordHasher::comparePassword($digest, $hash);
}
public function newPasswordEnvelope() {
return new PhutilOpaqueEnvelope($this->getPasswordHash());
}
private function digestPassword(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object) {
$object_phid = $object->getPHID();
if ($this->getObjectPHID() !== $object->getPHID()) {
throw new Exception(
pht(
'This password is associated with an object PHID ("%s") for '.
'a different object than the provided one ("%s").',
$this->getObjectPHID(),
$object->getPHID()));
}
$digest = $object->newPasswordDigest($password, $this);
if (!($digest instanceof PhutilOpaqueEnvelope)) {
throw new Exception(
pht(
'Failed to digest password: object ("%s") did not return an '.
'opaque envelope with a password digest.',
$object->getPHID()));
}
return $digest;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
- array($this->getObject(), PhabricatorPolicyCapability::CAN_VIEW),
+ array($this->getObject(), $capability),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthPasswordEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthPasswordTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php
index 2a9a6273b..5bbb7de83 100644
--- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php
+++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php
@@ -1,182 +1,176 @@
<?php
final class PhabricatorAuthSSHKey
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $objectPHID;
protected $name;
protected $keyType;
protected $keyIndex;
protected $keyBody;
protected $keyComment = '';
protected $isTrusted = 0;
protected $isActive;
private $object = self::ATTACHABLE;
public static function initializeNewSSHKey(
PhabricatorUser $viewer,
PhabricatorSSHPublicKeyInterface $object) {
// You must be able to edit an object to create a new key on it.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$object_phid = $object->getPHID();
return id(new self())
->setIsActive(1)
->setObjectPHID($object_phid)
->attachObject($object);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'keyType' => 'text255',
'keyIndex' => 'bytes12',
'keyBody' => 'text',
'keyComment' => 'text255',
'isTrusted' => 'bool',
'isActive' => 'bool?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
'key_active' => array(
'columns' => array('isActive', 'objectPHID'),
),
// NOTE: This unique key includes a nullable column, effectively
// constraining uniqueness on active keys only.
'key_activeunique' => array(
'columns' => array('keyIndex', 'isActive'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function save() {
$this->setKeyIndex($this->toPublicKey()->getHash());
return parent::save();
}
- public function getMailKey() {
- // NOTE: We don't actually receive mail for these objects. It's OK for
- // the mail key to be predictable until we do.
- return PhabricatorHash::digestForIndex($this->getPHID());
- }
-
public function toPublicKey() {
return PhabricatorAuthSSHPublicKey::newFromStoredKey($this);
}
public function getEntireKey() {
$parts = array(
$this->getKeyType(),
$this->getKeyBody(),
$this->getKeyComment(),
);
return trim(implode(' ', $parts));
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachObject(PhabricatorSSHPublicKeyInterface $object) {
$this->object = $object;
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthSSHKeyPHIDType::TYPECONST);
}
public function getURI() {
$id = $this->getID();
return "/auth/sshkey/view/{$id}/";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if (!$this->getIsActive()) {
if ($capability == PhabricatorPolicyCapability::CAN_EDIT) {
return PhabricatorPolicies::POLICY_NOONE;
}
}
return $this->getObject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$this->getIsActive()) {
return false;
}
return $this->getObject()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
if (!$this->getIsACtive()) {
return pht(
'Revoked SSH keys can not be edited or reinstated.');
}
return pht(
'SSH keys inherit the policies of the user or object they authenticate.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthSSHKeyEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthSSHKeyTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}
diff --git a/src/applications/cache/spec/PhabricatorDataCacheSpec.php b/src/applications/cache/spec/PhabricatorDataCacheSpec.php
index bf6a495f7..103b0d839 100644
--- a/src/applications/cache/spec/PhabricatorDataCacheSpec.php
+++ b/src/applications/cache/spec/PhabricatorDataCacheSpec.php
@@ -1,145 +1,158 @@
<?php
final class PhabricatorDataCacheSpec extends PhabricatorCacheSpec {
private $cacheSummary;
public function setCacheSummary(array $cache_summary) {
$this->cacheSummary = $cache_summary;
return $this;
}
public function getCacheSummary() {
return $this->cacheSummary;
}
public static function getActiveCacheSpec() {
$spec = new PhabricatorDataCacheSpec();
// NOTE: If APCu is installed, it reports that APC is installed.
if (extension_loaded('apc') && !extension_loaded('apcu')) {
$spec->initAPCSpec();
} else if (extension_loaded('apcu')) {
$spec->initAPCuSpec();
} else {
$spec->initNoneSpec();
}
return $spec;
}
private function initAPCSpec() {
$this
->setName(pht('APC User Cache'))
->setVersion(phpversion('apc'));
if (ini_get('apc.enabled')) {
$this
->setIsEnabled(true)
->setClearCacheCallback('apc_clear_cache');
$this->initAPCCommonSpec();
} else {
$this->setIsEnabled(false);
$this->raiseEnableAPCIssue();
}
}
private function initAPCuSpec() {
$this
->setName(pht('APCu'))
->setVersion(phpversion('apcu'));
if (ini_get('apc.enabled')) {
if (function_exists('apcu_clear_cache')) {
$clear_callback = 'apcu_clear_cache';
} else {
$clear_callback = 'apc_clear_cache';
}
$this
->setIsEnabled(true)
->setClearCacheCallback($clear_callback);
$this->initAPCCommonSpec();
} else {
$this->setIsEnabled(false);
$this->raiseEnableAPCIssue();
}
}
private function initNoneSpec() {
if (version_compare(phpversion(), '5.5', '>=')) {
$message = pht(
'Installing the "APCu" PHP extension will improve performance. '.
'This extension is strongly recommended. Without it, Phabricator '.
'must rely on a very inefficient disk-based cache.');
$this
->newIssue('extension.apcu')
->setShortName(pht('APCu'))
->setName(pht('PHP Extension "APCu" Not Installed'))
->setMessage($message)
->addPHPExtension('apcu');
} else {
$this->raiseInstallAPCIssue();
}
}
private function initAPCCommonSpec() {
$state = array();
if (function_exists('apcu_sma_info')) {
$mem = apcu_sma_info();
$info = apcu_cache_info();
} else if (function_exists('apc_sma_info')) {
$mem = apc_sma_info();
$info = apc_cache_info('user');
} else {
$mem = null;
}
if ($mem) {
$this->setTotalMemory($mem['num_seg'] * $mem['seg_size']);
$this->setUsedMemory($info['mem_size']);
$this->setEntryCount(count($info['cache_list']));
$cache = $info['cache_list'];
$state = array();
foreach ($cache as $item) {
- $info = idx($item, 'info', '<unknown-key>');
- $key = self::getKeyPattern($info);
+ // Some older versions of APCu report the cachekey as "key", while
+ // newer APCu and APC report it as "info". Just check both indexes
+ // for commpatibility. See T13164 for details.
+
+ $info = idx($item, 'info');
+ if ($info === null) {
+ $info = idx($item, 'key');
+ }
+
+ if ($info === null) {
+ $key = '<unknown-key>';
+ } else {
+ $key = self::getKeyPattern($info);
+ }
+
if (empty($state[$key])) {
$state[$key] = array(
'max' => 0,
'total' => 0,
'count' => 0,
);
}
$state[$key]['max'] = max($state[$key]['max'], $item['mem_size']);
$state[$key]['total'] += $item['mem_size'];
$state[$key]['count']++;
}
}
$this->setCacheSummary($state);
}
private static function getKeyPattern($key) {
// If this key isn't in the current cache namespace, don't reveal any
// information about it.
$namespace = PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
if (strncmp($key, $namespace.':', strlen($namespace) + 1)) {
return '<other-namespace>';
}
$key = preg_replace('/(?<![a-zA-Z])\d+(?![a-zA-Z])/', 'N', $key);
$key = preg_replace('/PHID-[A-Z]{4}-[a-z0-9]{20}/', 'PHID', $key);
// TODO: We should probably standardize how digests get embedded into cache
// keys to make this rule more generic.
$key = preg_replace('/:celerity:.*$/', ':celerity:X', $key);
$key = preg_replace('/:pkcs8:.*$/', ':pkcs8:X', $key);
return $key;
}
}
diff --git a/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php b/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php
index 37c02a437..2a59d7e8a 100644
--- a/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php
+++ b/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php
@@ -1,206 +1,206 @@
<?php
final class PhabricatorOpcodeCacheSpec extends PhabricatorCacheSpec {
public static function getActiveCacheSpec() {
$spec = new PhabricatorOpcodeCacheSpec();
// NOTE: If APCu is installed, it reports that APC is installed.
if (extension_loaded('apc') && !extension_loaded('apcu')) {
$spec->initAPCSpec();
} else if (extension_loaded('Zend OPcache')) {
$spec->initOpcacheSpec();
} else {
$spec->initNoneSpec();
}
return $spec;
}
private function initAPCSpec() {
$this
->setName(pht('APC'))
->setVersion(phpversion('apc'));
if (ini_get('apc.enabled')) {
$this
->setIsEnabled(true)
->setClearCacheCallback('apc_clear_cache');
$mem = apc_sma_info();
$this->setTotalMemory($mem['num_seg'] * $mem['seg_size']);
$info = apc_cache_info();
$this->setUsedMemory($info['mem_size']);
$write_lock = ini_get('apc.write_lock');
$slam_defense = ini_get('apc.slam_defense');
if (!$write_lock || $slam_defense) {
$summary = pht('Adjust APC settings to quiet unnecessary errors.');
$message = pht(
'Some versions of APC may emit unnecessary errors into the '.
'error log under the current APC settings. To resolve this, '.
'enable "%s" and disable "%s" in your PHP configuration.',
'apc.write_lock',
'apc.slam_defense');
$this
->newIssue('extension.apc.write-lock')
->setShortName(pht('Noisy APC'))
->setName(pht('APC Has Noisy Configuration'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.write_lock')
->addPHPConfig('apc.slam_defense');
}
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$is_stat_enabled = ini_get('apc.stat');
if ($is_stat_enabled && !$is_dev) {
$summary = pht(
'"%s" is currently enabled, but should probably be disabled.',
'apc.stat');
$message = pht(
'The "%s" setting is currently enabled in your PHP configuration. '.
'In production mode, "%s" should be disabled. '.
'This will improve performance slightly.',
'apc.stat',
'apc.stat');
$this
->newIssue('extension.apc.stat-enabled')
->setShortName(pht('"%s" Enabled', 'apc.stat'))
->setName(pht('"%s" Enabled in Production', 'apc.stat'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.stat')
->addPhabricatorConfig('phabricator.developer-mode');
} else if (!$is_stat_enabled && $is_dev) {
$summary = pht(
'"%s" is currently disabled, but should probably be enabled.',
'apc.stat');
$message = pht(
'The "%s" setting is currently disabled in your PHP configuration, '.
'but Phabricator is running in development mode. This option should '.
'normally be enabled in development so you do not need to restart '.
'anything after making changes to the code.',
'apc.stat');
$this
->newIssue('extension.apc.stat-disabled')
->setShortName(pht('"%s" Disabled', 'apc.stat'))
->setName(pht('"%s" Disabled in Development', 'apc.stat'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.stat')
->addPhabricatorConfig('phabricator.developer-mode');
}
} else {
$this->setIsEnabled(false);
$this->raiseEnableAPCIssue();
}
}
private function initOpcacheSpec() {
$this
->setName(pht('Zend OPcache'))
->setVersion(phpversion('Zend OPcache'));
if (ini_get('opcache.enable')) {
$this
->setIsEnabled(true)
->setClearCacheCallback('opcache_reset');
$status = opcache_get_status();
$memory = $status['memory_usage'];
$mem_used = $memory['used_memory'];
$mem_free = $memory['free_memory'];
$mem_junk = $memory['wasted_memory'];
$this->setUsedMemory($mem_used + $mem_junk);
$this->setTotalMemory($mem_used + $mem_junk + $mem_free);
$this->setEntryCount($status['opcache_statistics']['num_cached_keys']);
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$validate = ini_get('opcache.validate_timestamps');
$freq = ini_get('opcache.revalidate_freq');
if ($is_dev && (!$validate || $freq)) {
$summary = pht(
'OPcache is not configured properly for development.');
$message = pht(
'In development, OPcache should be configured to always reload '.
'code so nothing needs to be restarted after making changes. To do '.
'this, enable "%s" and set "%s" to 0.',
'opcache.validate_timestamps',
'opcache.revalidate_freq');
$this
->newIssue('extension.opcache.devmode')
->setShortName(pht('OPcache Config'))
- ->setName(pht('OPCache Not Configured for Development'))
+ ->setName(pht('OPcache Not Configured for Development'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('opcache.validate_timestamps')
->addPHPConfig('opcache.revalidate_freq')
->addPhabricatorConfig('phabricator.developer-mode');
} else if (!$is_dev && $validate) {
$summary = pht('OPcache is not configured ideally for production.');
$message = pht(
'In production, OPcache should be configured to never '.
'revalidate code. This will slightly improve performance. '.
'To do this, disable "%s" in your PHP configuration.',
'opcache.validate_timestamps');
$this
->newIssue('extension.opcache.production')
->setShortName(pht('OPcache Config'))
->setName(pht('OPcache Not Configured for Production'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('opcache.validate_timestamps')
->addPhabricatorConfig('phabricator.developer-mode');
}
} else {
$this->setIsEnabled(false);
$summary = pht('Enabling OPcache will dramatically improve performance.');
$message = pht(
'The PHP "Zend OPcache" extension is installed, but not enabled in '.
'your PHP configuration. Enabling it will dramatically improve '.
'Phabricator performance. Edit the "%s" setting to '.
'enable the extension.',
'opcache.enable');
$this->newIssue('extension.opcache.enable')
->setShortName(pht('OPcache Disabled'))
->setName(pht('Zend OPcache Not Enabled'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('opcache.enable');
}
}
private function initNoneSpec() {
if (version_compare(phpversion(), '5.5', '>=')) {
$message = pht(
'Installing the "Zend OPcache" extension will dramatically improve '.
'performance.');
$this
->newIssue('extension.opcache')
->setShortName(pht('OPcache'))
->setName(pht('Zend OPcache Not Installed'))
->setMessage($message)
->addPHPExtension('Zend OPcache');
} else {
$this->raiseInstallAPCIssue();
}
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
index 12127a1e2..aea3fc090 100644
--- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
@@ -1,711 +1,716 @@
<?php
final class PhabricatorConduitAPIController
extends PhabricatorConduitController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$method = $request->getURIData('method');
$time_start = microtime(true);
$api_request = null;
$method_implementation = null;
$log = new PhabricatorConduitMethodCallLog();
$log->setMethod($method);
$metadata = array();
$multimeter = MultimeterControl::getInstance();
if ($multimeter) {
$multimeter->setEventContext('api.'.$method);
}
try {
list($metadata, $params, $strictly_typed) = $this->decodeConduitParams(
$request,
$method);
$call = new ConduitCall($method, $params, $strictly_typed);
$method_implementation = $call->getMethodImplementation();
$result = null;
// TODO: The relationship between ConduitAPIRequest and ConduitCall is a
// little odd here and could probably be improved. Specifically, the
// APIRequest is a sub-object of the Call, which does not parallel the
// role of AphrontRequest (which is an indepenent object).
// In particular, the setUser() and getUser() existing independently on
// the Call and APIRequest is very awkward.
$api_request = $call->getAPIRequest();
$allow_unguarded_writes = false;
$auth_error = null;
$conduit_username = '-';
if ($call->shouldRequireAuthentication()) {
$auth_error = $this->authenticateUser($api_request, $metadata, $method);
// If we've explicitly authenticated the user here and either done
// CSRF validation or are using a non-web authentication mechanism.
$allow_unguarded_writes = true;
if ($auth_error === null) {
$conduit_user = $api_request->getUser();
if ($conduit_user && $conduit_user->getPHID()) {
$conduit_username = $conduit_user->getUsername();
}
$call->setUser($api_request->getUser());
}
}
$access_log = PhabricatorAccessLog::getLog();
if ($access_log) {
$access_log->setData(
array(
'u' => $conduit_username,
'm' => $method,
));
}
if ($call->shouldAllowUnguardedWrites()) {
$allow_unguarded_writes = true;
}
if ($auth_error === null) {
if ($allow_unguarded_writes) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
}
try {
$result = $call->execute();
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
if ($ex->getErrorDescription()) {
$error_info = $ex->getErrorDescription();
} else {
$error_info = $call->getErrorDescription($error_code);
}
}
if ($allow_unguarded_writes) {
unset($unguarded);
}
} else {
list($error_code, $error_info) = $auth_error;
}
} catch (Exception $ex) {
if (!($ex instanceof ConduitMethodNotFoundException)) {
phlog($ex);
}
$result = null;
$error_code = ($ex instanceof ConduitException
? 'ERR-CONDUIT-CALL'
: 'ERR-CONDUIT-CORE');
$error_info = $ex->getMessage();
}
$time_end = microtime(true);
$log
->setCallerPHID(
isset($conduit_user)
? $conduit_user->getPHID()
: null)
->setError((string)$error_code)
->setDuration(1000000 * ($time_end - $time_start));
if (!PhabricatorEnv::isReadOnly()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$log->save();
unset($unguarded);
}
$response = id(new ConduitAPIResponse())
->setResult($result)
->setErrorCode($error_code)
->setErrorInfo($error_info);
switch ($request->getStr('output')) {
case 'human':
return $this->buildHumanReadableResponse(
$method,
$api_request,
$response->toDictionary(),
$method_implementation);
case 'json':
default:
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($response->toDictionary());
}
}
/**
* Authenticate the client making the request to a Phabricator user account.
*
* @param ConduitAPIRequest Request being executed.
* @param dict Request metadata.
* @return null|pair Null to indicate successful authentication, or
* an error code and error message pair.
*/
private function authenticateUser(
ConduitAPIRequest $api_request,
array $metadata,
$method) {
$request = $this->getRequest();
if ($request->getUser()->getPHID()) {
$request->validateCSRF();
return $this->validateAuthenticatedUser(
$api_request,
$request->getUser());
}
$auth_type = idx($metadata, 'auth.type');
if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) {
$host = idx($metadata, 'auth.host');
if (!$host) {
return array(
'ERR-INVALID-AUTH',
pht(
'Request is missing required "%s" parameter.',
'auth.host'),
);
}
// TODO: Validate that we are the host!
$raw_key = idx($metadata, 'auth.key');
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key);
$ssl_public_key = $public_key->toPKCS8();
// First, verify the signature.
try {
$protocol_data = $metadata;
ConduitClient::verifySignature(
$method,
$api_request->getAllParameters(),
$protocol_data,
$ssl_public_key);
} catch (Exception $ex) {
return array(
'ERR-INVALID-AUTH',
pht(
'Signature verification failure. %s',
$ex->getMessage()),
);
}
// If the signature is valid, find the user or device which is
// associated with this public key.
$stored_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withKeys(array($public_key))
->withIsActive(true)
->executeOne();
if (!$stored_key) {
+ $key_summary = id(new PhutilUTF8StringTruncator())
+ ->setMaximumBytes(64)
+ ->truncateString($raw_key);
return array(
'ERR-INVALID-AUTH',
- pht('No user or device is associated with that public key.'),
+ pht(
+ 'No user or device is associated with the public key "%s".',
+ $key_summary),
);
}
$object = $stored_key->getObject();
if ($object instanceof PhabricatorUser) {
$user = $object;
} else {
if (!$stored_key->getIsTrusted()) {
return array(
'ERR-INVALID-AUTH',
pht(
'The key which signed this request is not trusted. Only '.
'trusted keys can be used to sign API calls.'),
);
}
if (!PhabricatorEnv::isClusterRemoteAddress()) {
return array(
'ERR-INVALID-AUTH',
pht(
'This request originates from outside of the Phabricator '.
'cluster address range. Requests signed with trusted '.
'device keys must originate from within the cluster.'),
);
}
$user = PhabricatorUser::getOmnipotentUser();
// Flag this as an intracluster request.
$api_request->setIsClusterRequest(true);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
} else if ($auth_type === null) {
// No specified authentication type, continue with other authentication
// methods below.
} else {
return array(
'ERR-INVALID-AUTH',
pht(
'Provided "%s" ("%s") is not recognized.',
'auth.type',
$auth_type),
);
}
$token_string = idx($metadata, 'token');
if (strlen($token_string)) {
if (strlen($token_string) != 32) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong length. API tokens should be '.
'32 characters long.',
$token_string),
);
}
$type = head(explode('-', $token_string));
$valid_types = PhabricatorConduitToken::getAllTokenTypes();
$valid_types = array_fuse($valid_types);
if (empty($valid_types[$type])) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong format. API tokens should be '.
'32 characters long and begin with one of these prefixes: %s.',
$token_string,
implode(', ', $valid_types)),
);
}
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(false)
->executeOne();
if (!$token) {
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(true)
->executeOne();
if ($token) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" was previously valid, but has expired.',
$token_string),
);
} else {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" is not valid.',
$token_string),
);
}
}
// If this is a "cli-" token, it expires shortly after it is generated
// by default. Once it is actually used, we extend its lifetime and make
// it permanent. This allows stray tokens to get cleaned up automatically
// if they aren't being used.
if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) {
if ($token->getExpires()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token->setExpires(null);
$token->save();
unset($unguarded);
}
}
// If this is a "clr-" token, Phabricator must be configured in cluster
// mode and the remote address must be a cluster node.
if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) {
if (!PhabricatorEnv::isClusterRemoteAddress()) {
return array(
'ERR-INVALID-AUTH',
pht(
'This request originates from outside of the Phabricator '.
'cluster address range. Requests signed with cluster API '.
'tokens must originate from within the cluster.'),
);
}
// Flag this as an intracluster request.
$api_request->setIsClusterRequest(true);
}
$user = $token->getObject();
if (!($user instanceof PhabricatorUser)) {
return array(
'ERR-INVALID-AUTH',
pht('API token is not associated with a valid user.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
$access_token = idx($metadata, 'access_token');
if ($access_token) {
$token = id(new PhabricatorOAuthServerAccessToken())
->loadOneWhere('token = %s', $access_token);
if (!$token) {
return array(
'ERR-INVALID-AUTH',
pht('Access token does not exist.'),
);
}
$oauth_server = new PhabricatorOAuthServer();
$authorization = $oauth_server->authorizeToken($token);
if (!$authorization) {
return array(
'ERR-INVALID-AUTH',
pht('Access token is invalid or expired.'),
);
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($token->getUserPHID()))
->executeOne();
if (!$user) {
return array(
'ERR-INVALID-AUTH',
pht('Access token is for invalid user.'),
);
}
$ok = $this->authorizeOAuthMethodAccess($authorization, $method);
if (!$ok) {
return array(
'ERR-OAUTH-ACCESS',
pht('You do not have authorization to call this method.'),
);
}
$api_request->setOAuthToken($token);
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// For intracluster requests, use a public user if no authentication
// information is provided. We could do this safely for any request,
// but making the API fully public means there's no way to disable badly
// behaved clients.
if (PhabricatorEnv::isClusterRemoteAddress()) {
if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$api_request->setIsClusterRequest(true);
$user = new PhabricatorUser();
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
}
// Handle sessionless auth.
// TODO: This is super messy.
// TODO: Remove this in favor of token-based auth.
if (isset($metadata['authUser'])) {
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$metadata['authUser']);
if (!$user) {
return array(
'ERR-INVALID-AUTH',
pht('Authentication is invalid.'),
);
}
$token = idx($metadata, 'authToken');
$signature = idx($metadata, 'authSignature');
$certificate = $user->getConduitCertificate();
$hash = sha1($token.$certificate);
if (!phutil_hashes_are_identical($hash, $signature)) {
return array(
'ERR-INVALID-AUTH',
pht('Authentication is invalid.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// Handle session-based auth.
// TODO: Remove this in favor of token-based auth.
$session_key = idx($metadata, 'sessionKey');
if (!$session_key) {
return array(
'ERR-INVALID-SESSION',
pht('Session key is not present.'),
);
}
$user = id(new PhabricatorAuthSessionEngine())
->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key);
if (!$user) {
return array(
'ERR-INVALID-SESSION',
pht('Session key is invalid.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
private function validateAuthenticatedUser(
ConduitAPIRequest $request,
PhabricatorUser $user) {
if (!$user->canEstablishAPISessions()) {
return array(
'ERR-INVALID-AUTH',
pht('User account is not permitted to use the API.'),
);
}
$request->setUser($user);
id(new PhabricatorAuthSessionEngine())
->willServeRequestForUser($user);
return null;
}
private function buildHumanReadableResponse(
$method,
ConduitAPIRequest $request = null,
$result = null,
ConduitAPIMethod $method_implementation = null) {
$param_rows = array();
$param_rows[] = array('Method', $this->renderAPIValue($method));
if ($request) {
foreach ($request->getAllParameters() as $key => $value) {
$param_rows[] = array(
$key,
$this->renderAPIValue($value),
);
}
}
$param_table = new AphrontTableView($param_rows);
$param_table->setColumnClasses(
array(
'header',
'wide',
));
$result_rows = array();
foreach ($result as $key => $value) {
$result_rows[] = array(
$key,
$this->renderAPIValue($value),
);
}
$result_table = new AphrontTableView($result_rows);
$result_table->setColumnClasses(
array(
'header',
'wide',
));
$param_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Method Parameters'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($param_table);
$result_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Method Result'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($result_table);
$method_uri = $this->getApplicationURI('method/'.$method.'/');
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($method, $method_uri)
->addTextCrumb(pht('Call'))
->setBorder(true);
$example_panel = null;
if ($request && $method_implementation) {
$params = $request->getAllParameters();
$example_panel = $this->renderExampleBox(
$method_implementation,
$params);
}
$title = pht('Method Call Result');
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-exchange');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$param_panel,
$result_panel,
$example_panel,
));
$title = pht('Method Call Result');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function renderAPIValue($value) {
$json = new PhutilJSON();
if (is_array($value)) {
$value = $json->encodeFormatted($value);
}
$value = phutil_tag(
'pre',
array('style' => 'white-space: pre-wrap;'),
$value);
return $value;
}
private function decodeConduitParams(
AphrontRequest $request,
$method) {
// Look for parameters from the Conduit API Console, which are encoded
// as HTTP POST parameters in an array, e.g.:
//
// params[name]=value¶ms[name2]=value2
//
// The fields are individually JSON encoded, since we require users to
// enter JSON so that we avoid type ambiguity.
$params = $request->getArr('params', null);
if ($params !== null) {
foreach ($params as $key => $value) {
if ($value == '') {
// Interpret empty string null (e.g., the user didn't type anything
// into the box).
$value = 'null';
}
$decoded_value = json_decode($value, true);
if ($decoded_value === null && strtolower($value) != 'null') {
// When json_decode() fails, it returns null. This almost certainly
// indicates that a user was using the web UI and didn't put quotes
// around a string value. We can either do what we think they meant
// (treat it as a string) or fail. For now, err on the side of
// caution and fail. In the future, if we make the Conduit API
// actually do type checking, it might be reasonable to treat it as
// a string if the parameter type is string.
throw new Exception(
pht(
"The value for parameter '%s' is not valid JSON. All ".
"parameters must be encoded as JSON values, including strings ".
"(which means you need to surround them in double quotes). ".
"Check your syntax. Value was: %s.",
$key,
$value));
}
$params[$key] = $decoded_value;
}
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params, true);
}
// Otherwise, look for a single parameter called 'params' which has the
// entire param dictionary JSON encoded.
$params_json = $request->getStr('params');
if (strlen($params_json)) {
$params = null;
try {
$params = phutil_json_decode($params_json);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
"Invalid parameter information was passed to method '%s'.",
$method),
$ex);
}
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params, true);
}
// If we do not have `params`, assume this is a simple HTTP request with
// HTTP key-value pairs.
$params = array();
$metadata = array();
foreach ($request->getPassthroughRequestData() as $key => $value) {
$meta_key = ConduitAPIMethod::getParameterMetadataKey($key);
if ($meta_key !== null) {
$metadata[$meta_key] = $value;
} else {
$params[$key] = $value;
}
}
return array($metadata, $params, false);
}
private function authorizeOAuthMethodAccess(
PhabricatorOAuthClientAuthorization $authorization,
$method_name) {
$method = ConduitAPIMethod::getConduitMethod($method_name);
if (!$method) {
return false;
}
$required_scope = $method->getRequiredScope();
switch ($required_scope) {
case ConduitAPIMethod::SCOPE_ALWAYS:
return true;
case ConduitAPIMethod::SCOPE_NEVER:
return false;
}
$authorization_scope = $authorization->getScope();
if (!empty($authorization_scope[$required_scope])) {
return true;
}
return false;
}
}
diff --git a/src/applications/conduit/data/ConduitConstantDescription.php b/src/applications/conduit/data/ConduitConstantDescription.php
new file mode 100644
index 000000000..fe2af2b71
--- /dev/null
+++ b/src/applications/conduit/data/ConduitConstantDescription.php
@@ -0,0 +1,26 @@
+<?php
+
+final class ConduitConstantDescription extends Phobject {
+
+ private $key;
+ private $value;
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+}
diff --git a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
index 6cb3bd240..f9ba48b37 100644
--- a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
+++ b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
@@ -1,66 +1,89 @@
<?php
final class PhabricatorConduitCallManagementWorkflow
extends PhabricatorConduitManagementWorkflow {
protected function didConstruct() {
$this
->setName('call')
->setSynopsis(pht('Call a Conduit method..'))
->setArguments(
array(
array(
'name' => 'method',
'param' => 'method',
'help' => pht('Method to call.'),
),
array(
'name' => 'input',
'param' => 'input',
'help' => pht(
'File to read parameters from, or "-" to read from '.
'stdin.'),
),
+ array(
+ 'name' => 'as',
+ 'param' => 'username',
+ 'help' => pht(
+ 'Execute the call as the given user. (If omitted, the call will '.
+ 'be executed as an omnipotent user.)'),
+ ),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$method = $args->getArg('method');
if (!strlen($method)) {
throw new PhutilArgumentUsageException(
pht('Specify a method to call with "--method".'));
}
$input = $args->getArg('input');
if (!strlen($input)) {
throw new PhutilArgumentUsageException(
pht('Specify a file to read parameters from with "--input".'));
}
+ $as = $args->getArg('as');
+ if (strlen($as)) {
+ $actor = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withUsernames(array($as))
+ ->executeOne();
+ if (!$actor) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'No such user "%s" exists.',
+ $as));
+ }
+ } else {
+ $actor = $viewer;
+ }
+
if ($input === '-') {
fprintf(STDERR, tsprintf("%s\n", pht('Reading input from stdin...')));
$input_json = file_get_contents('php://stdin');
} else {
$input_json = Filesystem::readFile($input);
}
$params = phutil_json_decode($input_json);
$result = id(new ConduitCall($method, $params))
- ->setUser($viewer)
+ ->setUser($actor)
->execute();
$output = array(
'result' => $result,
);
echo tsprintf(
"%B\n",
id(new PhutilJSON())->encodeFormatted($output));
return 0;
}
}
diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php
index 9789b7de9..e7540ad9d 100644
--- a/src/applications/config/application/PhabricatorConfigApplication.php
+++ b/src/applications/config/application/PhabricatorConfigApplication.php
@@ -1,78 +1,78 @@
<?php
final class PhabricatorConfigApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/config/';
}
public function getIcon() {
return 'fa-sliders';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return $viewer->getIsAdmin();
}
public function getTitleGlyph() {
- return "\xE2\x98\xBA";
+ return "\xE2\x9C\xA8";
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function canUninstall() {
return false;
}
public function getName() {
return pht('Config');
}
public function getShortDescription() {
return pht('Configure Phabricator');
}
public function getRoutes() {
return array(
'/config/' => array(
'' => 'PhabricatorConfigListController',
'application/' => 'PhabricatorConfigApplicationController',
'all/' => 'PhabricatorConfigAllController',
'history/' => 'PhabricatorConfigHistoryController',
'edit/(?P<key>[\w\.\-]+)/' => 'PhabricatorConfigEditController',
'group/(?P<key>[^/]+)/' => 'PhabricatorConfigGroupController',
'version/' => 'PhabricatorConfigVersionController',
'database/'.
'(?:(?P<ref>[^/]+)/'.
'(?:(?P<database>[^/]+)/'.
'(?:(?P<table>[^/]+)/'.
'(?:(?:col/(?P<column>[^/]+)|key/(?P<key>[^/]+))/)?)?)?)?'
=> 'PhabricatorConfigDatabaseStatusController',
'dbissue/' => 'PhabricatorConfigDatabaseIssueController',
'(?P<verb>ignore|unignore)/(?P<key>[^/]+)/'
=> 'PhabricatorConfigIgnoreController',
'issue/' => array(
'' => 'PhabricatorConfigIssueListController',
'panel/' => 'PhabricatorConfigIssuePanelController',
'(?P<key>[^/]+)/' => 'PhabricatorConfigIssueViewController',
),
'cache/' => array(
'' => 'PhabricatorConfigCacheController',
'purge/' => 'PhabricatorConfigPurgeCacheController',
),
'module/' => array(
'(?P<module>[^/]+)/' => 'PhabricatorConfigModuleController',
),
'cluster/' => array(
'databases/' => 'PhabricatorConfigClusterDatabasesController',
'notifications/' => 'PhabricatorConfigClusterNotificationsController',
'repositories/' => 'PhabricatorConfigClusterRepositoriesController',
'search/' => 'PhabricatorConfigClusterSearchController',
),
),
);
}
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index 84fd5bedf..4bf51602b 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,429 +1,362 @@
<?php
final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$ancient_config = self::getAncientConfig();
$all_keys = PhabricatorEnv::getAllConfigKeys();
$all_keys = array_keys($all_keys);
sort($all_keys);
$defined_keys = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($all_keys as $key) {
if (isset($defined_keys[$key])) {
continue;
}
if (isset($ancient_config[$key])) {
$summary = pht(
'This option has been removed. You may delete it at your '.
'convenience.');
$message = pht(
"The configuration option '%s' has been removed. You may delete ".
"it at your convenience.".
"\n\n%s",
$key,
$ancient_config[$key]);
$short = pht('Obsolete Config');
$name = pht('Obsolete Configuration Option "%s"', $key);
} else {
$summary = pht('This option is not recognized. It may be misspelled.');
$message = pht(
"The configuration option '%s' is not recognized. It may be ".
"misspelled, or it might have existed in an older version of ".
"Phabricator. It has no effect, and should be corrected or deleted.",
$key);
$short = pht('Unknown Config');
$name = pht('Unknown Configuration Option "%s"', $key);
}
$issue = $this->newIssue('config.unknown.'.$key)
->setShortName($short)
->setName($name)
->setSummary($summary);
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
$found = array();
$found_local = false;
$found_database = false;
foreach ($stack as $source_key => $source) {
$value = $source->getKeys(array($key));
if ($value) {
$found[] = $source->getName();
if ($source instanceof PhabricatorConfigDatabaseSource) {
$found_database = true;
}
if ($source instanceof PhabricatorConfigLocalSource) {
$found_local = true;
}
}
}
$message = $message."\n\n".pht(
'This configuration value is defined in these %d '.
'configuration source(s): %s.',
count($found),
implode(', ', $found));
$issue->setMessage($message);
if ($found_local) {
$command = csprintf('phabricator/ $ ./bin/config delete %s', $key);
$issue->addCommand($command);
}
if ($found_database) {
$issue->addPhabricatorConfig($key);
}
}
-
- $this->executeManiphestFieldChecks();
}
/**
* Return a map of deleted config options. Keys are option keys; values are
* explanations of what happened to the option.
*/
public static function getAncientConfig() {
$reason_auth = pht(
'This option has been migrated to the "Auth" application. Your old '.
'configuration is still in effect, but now stored in "Auth" instead of '.
'configuration. Going forward, you can manage authentication from '.
'the web UI.');
$auth_config = array(
'controller.oauth-registration',
'auth.password-auth-enabled',
'facebook.auth-enabled',
'facebook.registration-enabled',
'facebook.auth-permanent',
'facebook.application-id',
'facebook.application-secret',
'facebook.require-https-auth',
'github.auth-enabled',
'github.registration-enabled',
'github.auth-permanent',
'github.application-id',
'github.application-secret',
'google.auth-enabled',
'google.registration-enabled',
'google.auth-permanent',
'google.application-id',
'google.application-secret',
'ldap.auth-enabled',
'ldap.hostname',
'ldap.port',
'ldap.base_dn',
'ldap.search_attribute',
'ldap.search-first',
'ldap.username-attribute',
'ldap.real_name_attributes',
'ldap.activedirectory_domain',
'ldap.version',
'ldap.referrals',
'ldap.anonymous-user-name',
'ldap.anonymous-user-password',
'ldap.start-tls',
'disqus.auth-enabled',
'disqus.registration-enabled',
'disqus.auth-permanent',
'disqus.application-id',
'disqus.application-secret',
'phabricator.oauth-uri',
'phabricator.auth-enabled',
'phabricator.registration-enabled',
'phabricator.auth-permanent',
'phabricator.application-id',
'phabricator.application-secret',
);
$ancient_config = array_fill_keys($auth_config, $reason_auth);
$markup_reason = pht(
'Custom remarkup rules are now added by subclassing '.
'%s or %s.',
'PhabricatorRemarkupCustomInlineRule',
'PhabricatorRemarkupCustomBlockRule');
$session_reason = pht(
'Sessions now expire and are garbage collected rather than having an '.
'arbitrary concurrency limit.');
$differential_field_reason = pht(
'All Differential fields are now managed through the configuration '.
'option "%s". Use that option to configure which fields are shown.',
'differential.fields');
$reply_domain_reason = pht(
'Individual application reply handler domains have been removed. '.
'Configure a reply domain with "%s".',
'metamta.reply-handler-domain');
$reply_handler_reason = pht(
'Reply handlers can no longer be overridden with configuration.');
$monospace_reason = pht(
'Phabricator no longer supports global customization of monospaced '.
'fonts.');
$public_mail_reason = pht(
'Inbound mail addresses are now configured for each application '.
'in the Applications tool.');
$gc_reason = pht(
'Garbage collectors are now configured with "%s".',
'bin/garbage set-policy');
$aphlict_reason = pht(
'Configuration of the notification server has changed substantially. '.
'For discussion, see T10794.');
$stale_reason = pht(
'The Differential revision list view age UI elements have been removed '.
'to simplify the interface.');
$global_settings_reason = pht(
'The "Re: Prefix" and "Vary Subjects" settings are now configured '.
'in global settings.');
$dashboard_reason = pht(
'This option has been removed, you can use Dashboards to provide '.
'homepage customization. See T11533 for more details.');
$elastic_reason = pht(
'Elasticsearch is now configured with "%s".',
'cluster.search');
$ancient_config += array(
'phid.external-loaders' =>
pht(
'External loaders have been replaced. Extend `%s` '.
'to implement new PHID and handle types.',
'PhabricatorPHIDType'),
'maniphest.custom-task-extensions-class' =>
pht(
'Maniphest fields are now loaded automatically. '.
'You can configure them with `%s`.',
'maniphest.fields'),
'maniphest.custom-fields' =>
pht(
'Maniphest fields are now defined in `%s`. '.
'Existing definitions have been migrated.',
'maniphest.custom-field-definitions'),
'differential.custom-remarkup-rules' => $markup_reason,
'differential.custom-remarkup-block-rules' => $markup_reason,
'auth.sshkeys.enabled' => pht(
'SSH keys are now actually useful, so they are always enabled.'),
'differential.anonymous-access' => pht(
'Phabricator now has meaningful global access controls. See `%s`.',
'policy.allow-public'),
'celerity.resource-path' => pht(
'An alternate resource map is no longer supported. Instead, use '.
'multiple maps. See T4222.'),
'metamta.send-immediately' => pht(
'Mail is now always delivered by the daemons.'),
'auth.sessions.conduit' => $session_reason,
'auth.sessions.web' => $session_reason,
'tokenizer.ondemand' => pht(
'Phabricator now manages typeahead strategies automatically.'),
'differential.revision-custom-detail-renderer' => pht(
'Obsolete; use standard rendering events instead.'),
'differential.show-host-field' => $differential_field_reason,
'differential.show-test-plan-field' => $differential_field_reason,
'differential.field-selector' => $differential_field_reason,
'phabricator.show-beta-applications' => pht(
'This option has been renamed to `%s` to emphasize the '.
'unfinished nature of many prototype applications. '.
'Your existing setting has been migrated.',
'phabricator.show-prototypes'),
'notification.user' => pht(
'The notification server no longer requires root permissions. Start '.
'the server as the user you want it to run under.'),
'notification.debug' => pht(
'Notifications no longer have a dedicated debugging mode.'),
'translation.provider' => pht(
'The translation implementation has changed and providers are no '.
'longer used or supported.'),
'config.mask' => pht(
'Use `%s` instead of this option.',
'config.hide'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
'pool size with `%s`.',
'phd.taskmasters'),
'storage.engine-selector' => pht(
'Phabricator now automatically discovers available storage engines '.
'at runtime.'),
'storage.upload-size-limit' => pht(
'Phabricator now supports arbitrarily large files. Consult the '.
'documentation for configuration details.'),
'security.allow-outbound-http' => pht(
'This option has been replaced with the more granular option `%s`.',
'security.outbound-blacklist'),
'metamta.reply.show-hints' => pht(
'Phabricator no longer shows reply hints in mail.'),
'metamta.differential.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler-domain' => $reply_domain_reason,
'metamta.macro.reply-handler-domain' => $reply_domain_reason,
'metamta.maniphest.reply-handler-domain' => $reply_domain_reason,
'metamta.pholio.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler' => $reply_handler_reason,
'metamta.differential.reply-handler' => $reply_handler_reason,
'metamta.maniphest.reply-handler' => $reply_handler_reason,
'metamta.package.reply-handler' => $reply_handler_reason,
'metamta.precedence-bulk' => pht(
'Phabricator now always sends transaction mail with '.
'"Precedence: bulk" to improve deliverability.'),
'style.monospace' => $monospace_reason,
'style.monospace.windows' => $monospace_reason,
'search.engine-selector' => pht(
'Phabricator now automatically discovers available search engines '.
'at runtime.'),
'metamta.files.public-create-email' => $public_mail_reason,
'metamta.maniphest.public-create-email' => $public_mail_reason,
'metamta.maniphest.default-public-author' => $public_mail_reason,
'metamta.paste.public-create-email' => $public_mail_reason,
'security.allow-conduit-act-as-user' => pht(
'Impersonating users over the API is no longer supported.'),
'feed.public' => pht('The framable public feed is no longer supported.'),
'auth.login-message' => pht(
'This configuration option has been replaced with a modular '.
'handler. See T9346.'),
'gcdaemon.ttl.herald-transcripts' => $gc_reason,
'gcdaemon.ttl.daemon-logs' => $gc_reason,
'gcdaemon.ttl.differential-parse-cache' => $gc_reason,
'gcdaemon.ttl.markup-cache' => $gc_reason,
'gcdaemon.ttl.task-archive' => $gc_reason,
'gcdaemon.ttl.general-cache' => $gc_reason,
'gcdaemon.ttl.conduit-logs' => $gc_reason,
'phd.variant-config' => pht(
'This configuration is no longer relevant because daemons '.
'restart automatically on configuration changes.'),
'notification.ssl-cert' => $aphlict_reason,
'notification.ssl-key' => $aphlict_reason,
'notification.pidfile' => $aphlict_reason,
'notification.log' => $aphlict_reason,
'notification.enabled' => $aphlict_reason,
'notification.client-uri' => $aphlict_reason,
'notification.server-uri' => $aphlict_reason,
'metamta.differential.unified-comment-context' => pht(
'Inline comments are now always rendered with a limited amount '.
'of context.'),
'differential.days-fresh' => $stale_reason,
'differential.days-stale' => $stale_reason,
'metamta.re-prefix' => $global_settings_reason,
'metamta.vary-subjects' => $global_settings_reason,
'ui.custom-header' => pht(
'This option has been replaced with `ui.logo`, which provides more '.
'flexible configuration options.'),
'welcome.html' => $dashboard_reason,
'maniphest.priorities.unbreak-now' => $dashboard_reason,
'maniphest.priorities.needs-triage' => $dashboard_reason,
'mysql.implementation' => pht(
'Phabricator now automatically selects the best available '.
'MySQL implementation.'),
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
'search.elastic.host' => $elastic_reason,
'search.elastic.namespace' => $elastic_reason,
);
return $ancient_config;
}
- private function executeManiphestFieldChecks() {
- $maniphest_appclass = 'PhabricatorManiphestApplication';
- if (!PhabricatorApplication::isClassInstalled($maniphest_appclass)) {
- return;
- }
-
- $capabilities = array(
- ManiphestEditAssignCapability::CAPABILITY,
- ManiphestEditPoliciesCapability::CAPABILITY,
- ManiphestEditPriorityCapability::CAPABILITY,
- ManiphestEditProjectsCapability::CAPABILITY,
- ManiphestEditStatusCapability::CAPABILITY,
- );
-
- // Check for any of these capabilities set to anything other than
- // "All Users".
-
- $any_set = false;
- $app = new PhabricatorManiphestApplication();
- foreach ($capabilities as $capability) {
- $setting = $app->getPolicy($capability);
- if ($setting != PhabricatorPolicies::POLICY_USER) {
- $any_set = true;
- break;
- }
- }
-
- if (!$any_set) {
- return;
- }
-
- $issue_summary = pht(
- 'Maniphest is currently configured with deprecated policy settings '.
- 'which will be removed in a future version of Phabricator.');
-
-
- $message = pht(
- 'Some policy settings in Maniphest are now deprecated and will be '.
- 'removed in a future version of Phabricator. You are currently using '.
- 'at least one of these settings.'.
- "\n\n".
- 'The deprecated settings are "Can Assign Tasks", '.
- '"Can Edit Task Policies", "Can Prioritize Tasks", '.
- '"Can Edit Task Projects", and "Can Edit Task Status". You can '.
- 'find these settings in Applications, or follow the link below.'.
- "\n\n".
- 'You can find discussion of this change (including rationale and '.
- 'recommendations on how to configure similar features) in the upstream, '.
- 'at the link below.'.
- "\n\n".
- 'To resolve this issue, set all of these policies to "All Users" after '.
- 'making any necessary form customization changes.');
-
- $more_href = 'https://secure.phabricator.com/T10003';
- $edit_href = '/applications/view/PhabricatorManiphestApplication/';
-
- $issue = $this->newIssue('maniphest.T10003-per-field-policies')
- ->setShortName(pht('Deprecated Policies'))
- ->setName(pht('Deprecated Maniphest Field Policies'))
- ->setSummary($issue_summary)
- ->setMessage($message)
- ->addLink($more_href, pht('Learn More: Upstream Discussion'))
- ->addLink($edit_href, pht('Edit These Settings'));
- }
-
}
diff --git a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php
index 44db1b57a..c5c756e49 100644
--- a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php
+++ b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php
@@ -1,75 +1,146 @@
<?php
final class PhabricatorManualActivitySetupCheck
extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$activities = id(new PhabricatorConfigManualActivity())->loadAll();
foreach ($activities as $activity) {
$type = $activity->getActivityType();
- // For now, there is only one type of manual activity. It's not clear
- // if we're really going to have too much more of this stuff so this
- // is a bit under-designed for now.
-
- $activity_name = pht('Rebuild Search Index');
- $activity_summary = pht(
- 'The search index algorithm has been updated and the index needs '.
- 'be rebuilt.');
-
- $message = array();
-
- $message[] = pht(
- 'The indexing algorithm for the fulltext search index has been '.
- 'updated and the index needs to be rebuilt. Until you rebuild the '.
- 'index, global search (and other fulltext search) will not '.
- 'function correctly.');
-
- $message[] = pht(
- 'You can rebuild the search index while Phabricator is running.');
-
- $message[] = pht(
- 'To rebuild the index, run this command:');
-
- $message[] = phutil_tag(
- 'pre',
- array(),
- (string)csprintf(
- 'phabricator/ $ ./bin/search index --all --force --background'));
-
- $message[] = pht(
- 'You can find more information about rebuilding the search '.
- 'index here: %s',
- phutil_tag(
- 'a',
- array(
- 'href' => 'https://phurl.io/u/reindex',
- 'target' => '_blank',
- ),
- 'https://phurl.io/u/reindex'));
-
- $message[] = pht(
- 'After rebuilding the index, run this command to clear this setup '.
- 'warning:');
-
- $message[] = phutil_tag(
- 'pre',
- array(),
- (string)csprintf('phabricator/ $ ./bin/config done %R', $type));
-
- $activity_message = phutil_implode_html("\n\n", $message);
-
- $this->newIssue('manual.'.$type)
- ->setName($activity_name)
- ->setSummary($activity_summary)
- ->setMessage($activity_message);
+ switch ($type) {
+ case PhabricatorConfigManualActivity::TYPE_REINDEX:
+ $this->raiseSearchReindexIssue();
+ break;
+
+ case PhabricatorConfigManualActivity::TYPE_IDENTITIES:
+ $this->raiseRebuildIdentitiesIssue();
+ break;
+
+ default:
+ }
}
+ }
+
+ private function raiseSearchReindexIssue() {
+ $activity_name = pht('Rebuild Search Index');
+ $activity_summary = pht(
+ 'The search index algorithm has been updated and the index needs '.
+ 'be rebuilt.');
+
+ $message = array();
+
+ $message[] = pht(
+ 'The indexing algorithm for the fulltext search index has been '.
+ 'updated and the index needs to be rebuilt. Until you rebuild the '.
+ 'index, global search (and other fulltext search) will not '.
+ 'function correctly.');
+
+ $message[] = pht(
+ 'You can rebuild the search index while Phabricator is running.');
+
+ $message[] = pht(
+ 'To rebuild the index, run this command:');
+
+ $message[] = phutil_tag(
+ 'pre',
+ array(),
+ (string)csprintf(
+ 'phabricator/ $ ./bin/search index --all --force --background'));
+
+ $message[] = pht(
+ 'You can find more information about rebuilding the search '.
+ 'index here: %s',
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => 'https://phurl.io/u/reindex',
+ 'target' => '_blank',
+ ),
+ 'https://phurl.io/u/reindex'));
+
+ $message[] = pht(
+ 'After rebuilding the index, run this command to clear this setup '.
+ 'warning:');
+
+ $message[] = phutil_tag(
+ 'pre',
+ array(),
+ 'phabricator/ $ ./bin/config done reindex');
+
+ $activity_message = phutil_implode_html("\n\n", $message);
+
+ $this->newIssue('manual.reindex')
+ ->setName($activity_name)
+ ->setSummary($activity_summary)
+ ->setMessage($activity_message);
+ }
+ private function raiseRebuildIdentitiesIssue() {
+ $activity_name = pht('Rebuild Repository Identities');
+ $activity_summary = pht(
+ 'The mapping from VCS users to Phabricator users has changed '.
+ 'and must be rebuilt.');
+
+ $message = array();
+
+ $message[] = pht(
+ 'The way Phabricator attributes VCS activity to Phabricator users '.
+ 'has changed. There is a new indirection layer between the strings '.
+ 'that appear as VCS authors and committers (such as "John Developer '.
+ '<johnd@bigcorp.com>") and the Phabricator user that gets associated '.
+ 'with VCS commits. This is to support situations where users '.
+ 'are incorrectly associated with commits by Phabricator making bad '.
+ 'guesses about the identity of the corresponding Phabricator user. '.
+ 'This also helps with situations where existing repositories are '.
+ 'imported without having created accounts for all the committers to '.
+ 'that repository. Until you rebuild these repository identities, you '.
+ 'are likely to encounter problems with future Phabricator features '.
+ 'which will rely on the existence of these identities.');
+
+ $message[] = pht(
+ 'You can rebuild repository identities while Phabricator is running.');
+
+ $message[] = pht(
+ 'To rebuild identities, run this command:');
+
+ $message[] = phutil_tag(
+ 'pre',
+ array(),
+ (string)csprintf(
+ 'phabricator/ $ ./bin/repository rebuild-identities --all'));
+
+ $message[] = pht(
+ 'You can find more information about this new identity mapping '.
+ 'here: %s',
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => 'https://phurl.io/u/repoIdentities',
+ 'target' => '_blank',
+ ),
+ 'https://phurl.io/u/repoIdentities'));
+
+ $message[] = pht(
+ 'After rebuilding repository identities, run this command to clear '.
+ 'this setup warning:');
+
+ $message[] = phutil_tag(
+ 'pre',
+ array(),
+ 'phabricator/ $ ./bin/config done identities');
+
+ $activity_message = phutil_implode_html("\n\n", $message);
+
+ $this->newIssue('manual.identities')
+ ->setName($activity_name)
+ ->setSummary($activity_summary)
+ ->setMessage($activity_message);
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php
index 06df0de88..224705e18 100644
--- a/src/applications/config/controller/PhabricatorConfigEditController.php
+++ b/src/applications/config/controller/PhabricatorConfigEditController.php
@@ -1,506 +1,491 @@
<?php
final class PhabricatorConfigEditController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$key = $request->getURIData('key');
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
if (empty($options[$key])) {
$ancient = PhabricatorExtraConfigSetupCheck::getAncientConfig();
if (isset($ancient[$key])) {
$desc = pht(
"This configuration has been removed. You can safely delete ".
"it.\n\n%s",
$ancient[$key]);
} else {
$desc = pht(
'This configuration option is unknown. It may be misspelled, '.
'or have existed in a previous version of Phabricator.');
}
// This may be a dead config entry, which existed in the past but no
// longer exists. Allow it to be edited so it can be reviewed and
// deleted.
$option = id(new PhabricatorConfigOption())
->setKey($key)
->setType('wild')
->setDefault(null)
->setDescription($desc);
$group = null;
$group_uri = $this->getApplicationURI();
} else {
$option = $options[$key];
$group = $option->getGroup();
$group_uri = $this->getApplicationURI('group/'.$group->getKey().'/');
}
$issue = $request->getStr('issue');
if ($issue) {
// If the user came here from an open setup issue, send them back.
$done_uri = $this->getApplicationURI('issue/'.$issue.'/');
} else {
$done_uri = $group_uri;
}
// Check if the config key is already stored in the database.
// Grab the value if it is.
$config_entry = id(new PhabricatorConfigEntry())
->loadOneWhere(
'configKey = %s AND namespace = %s',
$key,
'default');
if (!$config_entry) {
$config_entry = id(new PhabricatorConfigEntry())
->setConfigKey($key)
->setNamespace('default')
->setIsDeleted(true);
$config_entry->setPHID($config_entry->generatePHID());
}
$e_value = null;
$errors = array();
if ($request->isFormPost() && !$option->getLocked()) {
$result = $this->readRequest(
$option,
$request);
list($e_value, $value_errors, $display_value, $xaction) = $result;
$errors = array_merge($errors, $value_errors);
if (!$errors) {
$editor = id(new PhabricatorConfigEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
try {
$editor->applyTransactions($config_entry, array($xaction));
return id(new AphrontRedirectResponse())->setURI($done_uri);
} catch (PhabricatorConfigValidationException $ex) {
$e_value = pht('Invalid');
$errors[] = $ex->getMessage();
}
}
} else {
if ($config_entry->getIsDeleted()) {
$display_value = null;
} else {
$display_value = $this->getDisplayValue(
$option,
$config_entry,
$config_entry->getValue());
}
}
$form = id(new AphrontFormView())
->setEncType('multipart/form-data');
$error_view = null;
if ($errors) {
$error_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_ERROR)
->setErrors($errors);
}
$doc_href = PhabricatorEnv::getDoclink(
'Configuration Guide: Locked and Hidden Configuration');
$doc_link = phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Learn more about locked and hidden options.'));
$status_items = array();
$tag = null;
if ($option->getHidden()) {
$tag = id(new PHUITagView())
->setName(pht('Hidden'))
->setColor(PHUITagView::COLOR_GREY)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_SHADE);
$message = pht(
'This configuration is hidden and can not be edited or viewed from '.
'the web interface.');
$status_items[] = id(new PHUIInfoView())
->appendChild(array($message, ' ', $doc_link));
} else if ($option->getLocked()) {
$tag = id(new PHUITagView())
->setName(pht('Locked'))
->setColor(PHUITagView::COLOR_RED)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_SHADE);
$message = $option->getLockedMessage();
$status_items[] = id(new PHUIInfoView())
->appendChild(array($message, ' ', $doc_link));
}
if ($option->getHidden() || $option->getLocked()) {
$controls = array();
} else {
$controls = $this->renderControls(
$option,
$display_value,
$e_value);
}
- $engine = new PhabricatorMarkupEngine();
- $engine->setViewer($viewer);
- $engine->addObject($option, 'description');
- $engine->process();
- $description = phutil_tag(
- 'div',
- array(
- 'class' => 'phabricator-remarkup',
- ),
- $engine->getOutput($option, 'description'));
-
$form
->setUser($viewer)
->addHiddenInput('issue', $request->getStr('issue'));
- $description = $option->getDescription();
- if (strlen($description)) {
- $description_view = new PHUIRemarkupView($viewer, $description);
-
- $form
- ->appendChild(
- id(new AphrontFormMarkupControl())
- ->setLabel(pht('Description'))
- ->setValue($description_view));
+ $description = $option->newDescriptionRemarkupView($viewer);
+ if ($description) {
+ $form->appendChild(
+ id(new AphrontFormMarkupControl())
+ ->setLabel(pht('Description'))
+ ->setValue($description));
}
if ($group) {
$extra = $group->renderContextualDescription(
$option,
$request);
if ($extra !== null) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setValue($extra));
}
}
foreach ($controls as $control) {
$form->appendControl($control);
}
if (!$option->getLocked()) {
$form->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($done_uri)
->setValue(pht('Save Config Entry')));
}
$current_config = null;
if (!$option->getHidden()) {
$current_config = $this->renderDefaults($option, $config_entry);
$current_config = $this->buildConfigBoxView(
pht('Current Configuration'),
$current_config);
}
$examples = $this->renderExamples($option);
if ($examples) {
$examples = $this->buildConfigBoxView(
pht('Examples'),
$examples);
}
$title = $key;
$box_header = array();
if ($group) {
$box_header[] = phutil_tag(
'a',
array(
'href' => $group_uri,
),
$group->getName());
$box_header[] = " \xC2\xBB ";
}
$box_header[] = $key;
$crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Config'), $this->getApplicationURI());
if ($group) {
$crumbs->addTextCrumb($group->getName(), $group_uri);
}
$crumbs->addTextCrumb($key, '/config/edit/'.$key);
$crumbs->setBorder(true);
$form_box = $this->buildConfigBoxView($box_header, $form, $tag);
$timeline = $this->buildTransactionTimeline(
$config_entry,
new PhabricatorConfigTransactionQuery());
$timeline->setShouldTerminate(true);
$nav = $this->buildSideNavView();
$nav->selectFilter($group_uri);
$header = $this->buildHeaderView($title);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setNavigation($nav)
->setFixed(true)
->setMainColumn(array(
$error_view,
$form_box,
$status_items,
$examples,
$current_config,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function readRequest(
PhabricatorConfigOption $option,
AphrontRequest $request) {
$type = $option->newOptionType();
if ($type) {
$is_set = $type->isValuePresentInRequest($option, $request);
if ($is_set) {
$value = $type->readValueFromRequest($option, $request);
$errors = array();
try {
$canonical_value = $type->newValueFromRequestValue(
$option,
$value);
$type->validateStoredValue($option, $canonical_value);
$xaction = $type->newTransaction($option, $canonical_value);
} catch (PhabricatorConfigValidationException $ex) {
$errors[] = $ex->getMessage();
$xaction = null;
} catch (Exception $ex) {
// NOTE: Some older validators throw bare exceptions. Purely in good
// taste, it would be nice to convert these at some point.
$errors[] = $ex->getMessage();
$xaction = null;
}
return array(
$errors ? pht('Invalid') : null,
$errors,
$value,
$xaction,
);
} else {
$delete_xaction = id(new PhabricatorConfigTransaction())
->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT)
->setNewValue(
array(
'deleted' => true,
'value' => null,
));
return array(
null,
array(),
null,
$delete_xaction,
);
}
}
// TODO: If we missed on the new `PhabricatorConfigType` map, fall back
// to the old semi-modular, semi-hacky way of doing things.
$xaction = new PhabricatorConfigTransaction();
$xaction->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT);
$e_value = null;
$errors = array();
if ($option->isCustomType()) {
$info = $option->getCustomObject()->readRequest($option, $request);
list($e_value, $errors, $set_value, $value) = $info;
} else {
throw new Exception(
pht(
'Unknown configuration option type "%s".',
$option->getType()));
}
if (!$errors) {
$xaction->setNewValue(
array(
'deleted' => false,
'value' => $set_value,
));
} else {
$xaction = null;
}
return array($e_value, $errors, $value, $xaction);
}
private function getDisplayValue(
PhabricatorConfigOption $option,
PhabricatorConfigEntry $entry,
$value) {
$type = $option->newOptionType();
if ($type) {
return $type->newDisplayValue($option, $value);
}
if ($option->isCustomType()) {
return $option->getCustomObject()->getDisplayValue(
$option,
$entry,
$value);
}
throw new Exception(
pht(
'Unknown configuration option type "%s".',
$option->getType()));
}
private function renderControls(
PhabricatorConfigOption $option,
$display_value,
$e_value) {
$type = $option->newOptionType();
if ($type) {
return $type->newControls(
$option,
$display_value,
$e_value);
}
if ($option->isCustomType()) {
$controls = $option->getCustomObject()->renderControls(
$option,
$display_value,
$e_value);
} else {
throw new Exception(
pht(
'Unknown configuration option type "%s".',
$option->getType()));
}
return $controls;
}
private function renderExamples(PhabricatorConfigOption $option) {
$examples = $option->getExamples();
if (!$examples) {
return null;
}
$table = array();
$table[] = phutil_tag('tr', array('class' => 'column-labels'), array(
phutil_tag('th', array(), pht('Example')),
phutil_tag('th', array(), pht('Value')),
));
foreach ($examples as $example) {
list($value, $description) = $example;
if ($value === null) {
$value = phutil_tag('em', array(), pht('(empty)'));
} else {
if (is_array($value)) {
$value = implode("\n", $value);
}
}
$table[] = phutil_tag('tr', array('class' => 'column-labels'), array(
phutil_tag('th', array(), $description),
phutil_tag('td', array(), $value),
));
}
require_celerity_resource('config-options-css');
return phutil_tag(
'table',
array(
'class' => 'config-option-table',
'cellspacing' => '0',
'cellpadding' => '0',
),
$table);
}
private function renderDefaults(
PhabricatorConfigOption $option,
PhabricatorConfigEntry $entry) {
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
$table = array();
$table[] = phutil_tag('tr', array('class' => 'column-labels'), array(
phutil_tag('th', array(), pht('Source')),
phutil_tag('th', array(), pht('Value')),
));
$is_effective_value = true;
foreach ($stack as $key => $source) {
$row_classes = array(
'column-labels',
);
$value = $source->getKeys(
array(
$option->getKey(),
));
if (!array_key_exists($option->getKey(), $value)) {
$value = phutil_tag('em', array(), pht('(No Value Configured)'));
} else {
$value = $this->getDisplayValue(
$option,
$entry,
$value[$option->getKey()]);
if ($is_effective_value) {
$is_effective_value = false;
$row_classes[] = 'config-options-effective-value';
}
}
$table[] = phutil_tag(
'tr',
array(
'class' => implode(' ', $row_classes),
),
array(
phutil_tag('th', array(), $source->getName()),
phutil_tag('td', array(), $value),
));
}
require_celerity_resource('config-options-css');
return phutil_tag(
'table',
array(
'class' => 'config-option-table',
'cellspacing' => '0',
'cellpadding' => '0',
),
$table);
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigGroupController.php b/src/applications/config/controller/PhabricatorConfigGroupController.php
index 920b2092a..7a3f77dfe 100644
--- a/src/applications/config/controller/PhabricatorConfigGroupController.php
+++ b/src/applications/config/controller/PhabricatorConfigGroupController.php
@@ -1,120 +1,113 @@
<?php
final class PhabricatorConfigGroupController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$group_key = $request->getURIData('key');
$groups = PhabricatorApplicationConfigOptions::loadAll();
$options = idx($groups, $group_key);
if (!$options) {
return new Aphront404Response();
}
$group_uri = PhabricatorConfigGroupConstants::getGroupFullURI(
$options->getGroup());
$group_name = PhabricatorConfigGroupConstants::getGroupShortName(
$options->getGroup());
$nav = $this->buildSideNavView();
$nav->selectFilter($group_uri);
$title = pht('%s Configuration', $options->getName());
$header = $this->buildHeaderView($title);
$list = $this->buildOptionList($options->getOptions());
$group_url = phutil_tag('a', array('href' => $group_uri), $group_name);
$box_header = pht("%s \xC2\xBB %s", $group_url, $options->getName());
$view = $this->buildConfigBoxView($box_header, $list);
$crumbs = $this->buildApplicationCrumbs()
- ->addTextCrumb($group_name, $this->getApplicationURI($group_uri))
+ ->addTextCrumb($group_name, $group_uri)
->addTextCrumb($options->getName())
->setBorder(true);
$content = id(new PHUITwoColumnView())
->setHeader($header)
->setNavigation($nav)
->setFixed(true)
->setMainColumn($view);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($content);
}
private function buildOptionList(array $options) {
assert_instances_of($options, 'PhabricatorConfigOption');
require_celerity_resource('config-options-css');
$db_values = array();
if ($options) {
$db_values = id(new PhabricatorConfigEntry())->loadAllWhere(
'configKey IN (%Ls) AND namespace = %s',
mpull($options, 'getKey'),
'default');
$db_values = mpull($db_values, null, 'getConfigKey');
}
- $engine = id(new PhabricatorMarkupEngine())
- ->setViewer($this->getRequest()->getUser());
- foreach ($options as $option) {
- $engine->addObject($option, 'summary');
- }
- $engine->process();
-
$list = new PHUIObjectItemListView();
$list->setBig(true);
foreach ($options as $option) {
- $summary = $engine->getOutput($option, 'summary');
+ $summary = $option->getSummary();
$item = id(new PHUIObjectItemView())
->setHeader($option->getKey())
->setHref('/config/edit/'.$option->getKey().'/')
->addAttribute($summary);
$color = null;
$db_value = idx($db_values, $option->getKey());
if ($db_value && !$db_value->getIsDeleted()) {
$item->setEffect('visited');
$color = 'violet';
}
if ($option->getHidden()) {
$item->setStatusIcon('fa-eye-slash grey', pht('Hidden'));
$item->setDisabled(true);
} else if ($option->getLocked()) {
$item->setStatusIcon('fa-lock '.$color, pht('Locked'));
} else if ($color) {
$item->setStatusIcon('fa-pencil '.$color, pht('Editable'));
} else {
$item->setStatusIcon('fa-pencil-square-o '.$color, pht('Editable'));
}
if (!$option->getHidden()) {
$current_value = PhabricatorEnv::getEnvConfig($option->getKey());
$current_value = PhabricatorConfigJSON::prettyPrintJSON(
$current_value);
$current_value = phutil_tag(
'div',
array(
'class' => 'config-options-current-value '.$color,
),
array(
$current_value,
));
$item->setSideColumn($current_value);
}
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php
index 770f79a9f..0ca94abe0 100644
--- a/src/applications/config/controller/PhabricatorConfigIssueListController.php
+++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php
@@ -1,119 +1,115 @@
<?php
final class PhabricatorConfigIssueListController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$nav = $this->buildSideNavView();
$nav->selectFilter('issue/');
$engine = new PhabricatorSetupEngine();
$response = $engine->execute();
if ($response) {
return $response;
}
$issues = $engine->getIssues();
$important = $this->buildIssueList(
$issues,
PhabricatorSetupCheck::GROUP_IMPORTANT,
'fa-warning');
$php = $this->buildIssueList(
$issues,
PhabricatorSetupCheck::GROUP_PHP,
'fa-code');
$mysql = $this->buildIssueList(
$issues,
PhabricatorSetupCheck::GROUP_MYSQL,
'fa-database');
$other = $this->buildIssueList(
$issues,
PhabricatorSetupCheck::GROUP_OTHER,
'fa-question-circle');
- $no_issues = null;
- if (empty($issues)) {
- $no_issues = id(new PHUIInfoView())
+ $title = pht('Setup Issues');
+ $header = $this->buildHeaderView($title);
+
+ if (!$issues) {
+ $issue_list = id(new PHUIInfoView())
->setTitle(pht('No Issues'))
->appendChild(
pht('Your install has no current setup issues to resolve.'))
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
+ } else {
+ $issue_list = array(
+ $important,
+ $php,
+ $mysql,
+ $other,
+ );
+
+ $issue_list = $this->buildConfigBoxView(pht('Issues'), $issue_list);
}
- $title = pht('Setup Issues');
- $header = $this->buildHeaderView($title);
-
- $issue_list = array(
- $important,
- $php,
- $mysql,
- $other,
- );
-
- $issue_list = $this->buildConfigBoxView(pht('Issues'), $issue_list);
-
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($title)
->setBorder(true);
$content = id(new PHUITwoColumnView())
->setHeader($header)
->setNavigation($nav)
->setFixed(true)
- ->setMainColumn(array(
- $no_issues,
- $issue_list,
- ));
+ ->setMainColumn($issue_list);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($content);
}
private function buildIssueList(array $issues, $group, $fonticon) {
assert_instances_of($issues, 'PhabricatorSetupIssue');
$list = new PHUIObjectItemListView();
$list->setBig(true);
$ignored_items = array();
$items = 0;
foreach ($issues as $issue) {
if ($issue->getGroup() == $group) {
$items++;
$href = $this->getApplicationURI('/issue/'.$issue->getIssueKey().'/');
$item = id(new PHUIObjectItemView())
->setHeader($issue->getName())
->setHref($href)
->addAttribute($issue->getSummary());
if (!$issue->getIsIgnored()) {
$icon = id(new PHUIIconView())
->setIcon($fonticon)
->setBackground('bg-sky');
$item->setImageIcon($icon);
$list->addItem($item);
} else {
$icon = id(new PHUIIconView())
->setIcon('fa-eye-slash')
->setBackground('bg-grey');
$item->setDisabled(true);
$item->setImageIcon($icon);
$ignored_items[] = $item;
}
}
}
foreach ($ignored_items as $item) {
$list->addItem($item);
}
if ($items == 0) {
return null;
} else {
return $list;
}
}
}
diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php
index 11b8de261..385af002c 100644
--- a/src/applications/config/option/PhabricatorConfigOption.php
+++ b/src/applications/config/option/PhabricatorConfigOption.php
@@ -1,246 +1,221 @@
<?php
final class PhabricatorConfigOption
- extends Phobject
- implements PhabricatorMarkupInterface {
+ extends Phobject {
private $key;
private $default;
private $summary;
private $description;
private $type;
private $boolOptions;
private $enumOptions;
private $group;
private $examples;
private $locked;
private $lockedMessage;
private $hidden;
private $baseClass;
private $customData;
private $customObject;
public function setBaseClass($base_class) {
$this->baseClass = $base_class;
return $this;
}
public function getBaseClass() {
return $this->baseClass;
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
if ($this->hidden) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.hide'),
$this->getKey(),
false);
}
public function setLocked($locked) {
$this->locked = $locked;
return $this;
}
public function getLocked() {
if ($this->locked) {
return true;
}
if ($this->getHidden()) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.lock'),
$this->getKey(),
false);
}
public function setLockedMessage($message) {
$this->lockedMessage = $message;
return $this;
}
public function getLockedMessage() {
if ($this->lockedMessage !== null) {
return $this->lockedMessage;
}
return pht(
'This configuration is locked and can not be edited from the web '.
'interface. Use %s in %s to edit it.',
phutil_tag('tt', array(), './bin/config'),
phutil_tag('tt', array(), 'phabricator/'));
}
public function addExample($value, $description) {
$this->examples[] = array($value, $description);
return $this;
}
public function getExamples() {
return $this->examples;
}
public function setGroup(PhabricatorApplicationConfigOptions $group) {
$this->group = $group;
return $this;
}
public function getGroup() {
return $this->group;
}
public function setBoolOptions(array $options) {
$this->boolOptions = $options;
return $this;
}
public function getBoolOptions() {
if ($this->boolOptions) {
return $this->boolOptions;
}
return array(
pht('True'),
pht('False'),
);
}
public function setEnumOptions(array $options) {
$this->enumOptions = $options;
return $this;
}
public function getEnumOptions() {
if ($this->enumOptions) {
return $this->enumOptions;
}
throw new PhutilInvalidStateException('setEnumOptions');
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setDefault($default) {
$this->default = $default;
return $this;
}
public function getDefault() {
return $this->default;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
if (empty($this->summary)) {
return $this->getDescription();
}
return $this->summary;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function newOptionType() {
$type_key = $this->getType();
$type_map = PhabricatorConfigType::getAllTypes();
return idx($type_map, $type_key);
}
public function isCustomType() {
return !strncmp($this->getType(), 'custom:', 7);
}
public function getCustomObject() {
if (!$this->customObject) {
if (!$this->isCustomType()) {
throw new Exception(pht('This option does not have a custom type!'));
}
$this->customObject = newv(substr($this->getType(), 7), array());
}
return $this->customObject;
}
public function getCustomData() {
return $this->customData;
}
public function setCustomData($data) {
$this->customData = $data;
return $this;
}
-/* -( PhabricatorMarkupInterface )----------------------------------------- */
-
- public function getMarkupFieldKey($field) {
- return $this->getKey().':'.$field;
- }
-
- public function newMarkupEngine($field) {
- return PhabricatorMarkupEngine::newMarkupEngine(array());
- }
-
- public function getMarkupText($field) {
- switch ($field) {
- case 'description':
- $text = $this->getDescription();
- break;
- case 'summary':
- $text = $this->getSummary();
- break;
+ public function newDescriptionRemarkupView(PhabricatorUser $viewer) {
+ $description = $this->getDescription();
+ if (!strlen($description)) {
+ return null;
}
- // TODO: We should probably implement this as a real Markup rule, but
- // markup rules are a bit of a mess right now and it doesn't hurt us to
- // fake this.
- $text = preg_replace(
+ // TODO: Some day, we should probably implement this as a real rule.
+ $description = preg_replace(
'/{{([^}]+)}}/',
'[[/config/edit/\\1/ | \\1]]',
- $text);
-
- return $text;
- }
-
- public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
- return $output;
- }
+ $description);
- public function shouldUseMarkupCache($field) {
- return false;
+ return new PHUIRemarkupView($viewer, $description);
}
}
diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php
index bfe7c148c..37fae45df 100644
--- a/src/applications/config/option/PhabricatorPHDConfigOptions.php
+++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php
@@ -1,95 +1,100 @@
<?php
final class PhabricatorPHDConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Daemons');
}
public function getDescription() {
return pht('Options relating to PHD (daemons).');
}
public function getIcon() {
return 'fa-pied-piper-alt';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('phd.pid-directory', 'string', '/var/tmp/phd/pid')
->setLocked(true)
->setDescription(
pht('Directory that phd should use to track running daemons.')),
$this->newOption('phd.log-directory', 'string', '/var/tmp/phd/log')
->setLocked(true)
->setDescription(
pht('Directory that the daemons should use to store log files.')),
$this->newOption('phd.taskmasters', 'int', 4)
->setLocked(true)
->setSummary(pht('Maximum taskmaster daemon pool size.'))
->setDescription(
pht(
- 'Maximum number of taskmaster daemons to run at once. Raising '.
- 'this can increase the maximum throughput of the task queue. The '.
- 'pool will automatically scale down when unutilized.')),
+ "Maximum number of taskmaster daemons to run at once. Raising ".
+ "this can increase the maximum throughput of the task queue. The ".
+ "pool will automatically scale down when unutilized.".
+ "\n\n".
+ "If you are running a cluster, this limit applies separately ".
+ "to each instance of `phd`. For example, if this limit is set ".
+ "to `4` and you have three hosts running daemons, the effective ".
+ "global limit will be 12.")),
$this->newOption('phd.verbose', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Verbose mode'),
pht('Normal mode'),
))
->setSummary(pht("Launch daemons in 'verbose' mode by default."))
->setDescription(
pht(
"Launch daemons in 'verbose' mode by default. This creates a lot ".
"of output, but can help debug issues. Daemons launched in debug ".
"mode with '%s' are always launched in verbose mode. ".
"See also '%s'.",
'phd debug',
'phd.trace')),
$this->newOption('phd.user', 'string', null)
->setLocked(true)
->setSummary(pht('System user to run daemons as.'))
->setDescription(
pht(
'Specify a system user to run the daemons as. Primarily, this '.
'user will own the working copies of any repositories that '.
'Phabricator imports or manages. This option is new and '.
'experimental.')),
$this->newOption('phd.trace', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Trace mode'),
pht('Normal mode'),
))
->setSummary(pht("Launch daemons in 'trace' mode by default."))
->setDescription(
pht(
"Launch daemons in 'trace' mode by default. This creates an ".
"ENORMOUS amount of output, but can help debug issues. Daemons ".
"launched in debug mode with '%s' are always launched in ".
"trace mode. See also '%s'.",
'phd debug',
'phd.verbose')),
$this->newOption('phd.garbage-collection', 'wild', array())
->setLocked(true)
->setLockedMessage(
pht(
'This option can not be edited from the web UI. Use %s to adjust '.
'garbage collector policies.',
phutil_tag('tt', array(), 'bin/garbage set-policy')))
->setSummary(pht('Retention policies for garbage collection.'))
->setDescription(
pht(
'Customizes retention policies for garbage collectors.')),
);
}
}
diff --git a/src/applications/config/storage/PhabricatorConfigManualActivity.php b/src/applications/config/storage/PhabricatorConfigManualActivity.php
index 44c26f77d..995202157 100644
--- a/src/applications/config/storage/PhabricatorConfigManualActivity.php
+++ b/src/applications/config/storage/PhabricatorConfigManualActivity.php
@@ -1,38 +1,40 @@
<?php
final class PhabricatorConfigManualActivity
extends PhabricatorConfigEntryDAO {
protected $activityType;
protected $parameters = array();
const TYPE_REINDEX = 'reindex';
+ const TYPE_IDENTITIES = 'identities';
+
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'activityType' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('activityType'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
}
diff --git a/src/applications/config/view/PhabricatorSetupIssueView.php b/src/applications/config/view/PhabricatorSetupIssueView.php
index 6fd75568a..74bdc2d52 100644
--- a/src/applications/config/view/PhabricatorSetupIssueView.php
+++ b/src/applications/config/view/PhabricatorSetupIssueView.php
@@ -1,614 +1,614 @@
<?php
final class PhabricatorSetupIssueView extends AphrontView {
private $issue;
public function setIssue(PhabricatorSetupIssue $issue) {
$this->issue = $issue;
return $this;
}
public function getIssue() {
return $this->issue;
}
public function renderInFlight() {
$issue = $this->getIssue();
return id(new PhabricatorInFlightErrorView())
->setMessage($issue->getName())
->render();
}
public function render() {
$issue = $this->getIssue();
$description = array();
$description[] = phutil_tag(
'div',
array(
'class' => 'setup-issue-instructions',
),
phutil_escape_html_newlines($issue->getMessage()));
$configs = $issue->getPHPConfig();
if ($configs) {
$description[] = $this->renderPHPConfig($configs, $issue);
}
$configs = $issue->getMySQLConfig();
if ($configs) {
$description[] = $this->renderMySQLConfig($configs);
}
$configs = $issue->getPhabricatorConfig();
if ($configs) {
$description[] = $this->renderPhabricatorConfig($configs);
}
$related_configs = $issue->getRelatedPhabricatorConfig();
if ($related_configs) {
$description[] = $this->renderPhabricatorConfig($related_configs,
$related = true);
}
$commands = $issue->getCommands();
if ($commands) {
$run_these = pht('Run these %d command(s):', count($commands));
$description[] = phutil_tag(
'div',
array(
'class' => 'setup-issue-config',
),
array(
phutil_tag('p', array(), $run_these),
phutil_tag('pre', array(), phutil_implode_html("\n", $commands)),
));
}
$extensions = $issue->getPHPExtensions();
if ($extensions) {
$install_these = pht(
'Install these %d PHP extension(s):', count($extensions));
$install_info = pht(
'You can usually install a PHP extension using %s or %s. Common '.
'package names are %s or %s. Try commands like these:',
phutil_tag('tt', array(), 'apt-get'),
phutil_tag('tt', array(), 'yum'),
hsprintf('<tt>php-<em>%s</em></tt>', pht('extname')),
hsprintf('<tt>php5-<em>%s</em></tt>', pht('extname')));
// TODO: We should do a better job of detecting how to install extensions
// on the current system.
$install_commands = hsprintf(
"\$ sudo apt-get install php5-<em>extname</em> ".
"# Debian / Ubuntu\n".
"\$ sudo yum install php-<em>extname</em> ".
"# Red Hat / Derivatives");
$fallback_info = pht(
"If those commands don't work, try Google. The process of installing ".
"PHP extensions is not specific to Phabricator, and any instructions ".
"you can find for installing them on your system should work. On Mac ".
"OS X, you might want to try Homebrew.");
$restart_info = pht(
'After installing new PHP extensions, <strong>restart Phabricator '.
'for the changes to take effect</strong>. For help with restarting '.
'Phabricator, see %s in the documentation.',
$this->renderRestartLink());
$description[] = phutil_tag(
'div',
array(
'class' => 'setup-issue-config',
),
array(
phutil_tag('p', array(), $install_these),
phutil_tag('pre', array(), implode("\n", $extensions)),
phutil_tag('p', array(), $install_info),
phutil_tag('pre', array(), $install_commands),
phutil_tag('p', array(), $fallback_info),
phutil_tag('p', array(), $restart_info),
));
}
$related_links = $issue->getLinks();
if ($related_links) {
$description[] = $this->renderRelatedLinks($related_links);
}
$actions = array();
if (!$issue->getIsFatal()) {
if ($issue->getIsIgnored()) {
$actions[] = javelin_tag(
'a',
array(
'href' => '/config/unignore/'.$issue->getIssueKey().'/',
'sigil' => 'workflow',
'class' => 'button button-grey',
),
pht('Unignore Setup Issue'));
} else {
$actions[] = javelin_tag(
'a',
array(
'href' => '/config/ignore/'.$issue->getIssueKey().'/',
'sigil' => 'workflow',
'class' => 'button button-grey',
),
pht('Ignore Setup Issue'));
}
$actions[] = javelin_tag(
'a',
array(
'href' => '/config/issue/'.$issue->getIssueKey().'/',
'class' => 'button button-grey',
),
pht('Reload Page'));
}
if ($actions) {
$actions = phutil_tag(
'div',
array(
'class' => 'setup-issue-actions',
),
$actions);
}
if ($issue->getIsIgnored()) {
$status = phutil_tag(
'div',
array(
'class' => 'setup-issue-status',
),
pht(
'This issue is currently ignored, and does not show a global '.
'warning.'));
$next = null;
} else {
$status = null;
$next = phutil_tag(
'div',
array(
'class' => 'setup-issue-next',
),
pht('To continue, resolve this problem and reload the page.'));
}
$name = phutil_tag(
'div',
array(
'class' => 'setup-issue-name',
),
$issue->getName());
$head = phutil_tag(
'div',
array(
'class' => 'setup-issue-head',
),
$name);
$body = phutil_tag(
'div',
array(
'class' => 'setup-issue-body',
),
array(
$status,
$description,
));
$tail = phutil_tag(
'div',
array(
'class' => 'setup-issue-tail',
),
$actions);
$issue = phutil_tag(
'div',
array(
'class' => 'setup-issue',
),
array(
$head,
$body,
$tail,
));
$debug_info = phutil_tag(
'div',
array(
'class' => 'setup-issue-debug',
),
pht('Host: %s', php_uname('n')));
return phutil_tag(
'div',
array(
'class' => 'setup-issue-shell',
),
array(
$issue,
$next,
$debug_info,
));
}
private function renderPhabricatorConfig(array $configs, $related = false) {
$issue = $this->getIssue();
$table_info = phutil_tag(
'p',
array(),
pht(
'The current Phabricator configuration has these %d value(s):',
count($configs)));
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
$hidden = array();
foreach ($options as $key => $option) {
if ($option->getHidden()) {
$hidden[$key] = true;
}
}
$table = null;
$dict = array();
foreach ($configs as $key) {
if (isset($hidden[$key])) {
$dict[$key] = null;
} else {
$dict[$key] = PhabricatorEnv::getUnrepairedEnvConfig($key);
}
}
$table = $this->renderValueTable($dict, $hidden);
if ($this->getIssue()->getIsFatal()) {
$update_info = phutil_tag(
'p',
array(),
pht(
'To update these %d value(s), run these command(s) from the command '.
'line:',
count($configs)));
$update = array();
foreach ($configs as $key) {
$update[] = hsprintf(
'<tt>phabricator/ $</tt> ./bin/config set %s <em>value</em>',
$key);
}
$update = phutil_tag('pre', array(), phutil_implode_html("\n", $update));
} else {
$update = array();
foreach ($configs as $config) {
if (idx($options, $config) && $options[$config]->getLocked()) {
$name = pht('View "%s"', $config);
} else {
$name = pht('Edit "%s"', $config);
}
$link = phutil_tag(
'a',
array(
'href' => '/config/edit/'.$config.'/?issue='.$issue->getIssueKey(),
),
$name);
$update[] = phutil_tag('li', array(), $link);
}
if ($update) {
$update = phutil_tag('ul', array(), $update);
if (!$related) {
$update_info = phutil_tag(
'p',
array(),
pht('You can update these %d value(s) here:', count($configs)));
} else {
$update_info = phutil_tag(
'p',
array(),
pht('These %d configuration value(s) are related:', count($configs)));
}
} else {
$update = null;
$update_info = null;
}
}
return phutil_tag(
'div',
array(
'class' => 'setup-issue-config',
),
array(
$table_info,
$table,
$update_info,
$update,
));
}
private function renderPHPConfig(array $configs, $issue) {
$table_info = phutil_tag(
'p',
array(),
pht(
'The current PHP configuration has these %d value(s):',
count($configs)));
$dict = array();
foreach ($configs as $key) {
$dict[$key] = $issue->getPHPConfigOriginalValue(
$key,
ini_get($key));
}
$table = $this->renderValueTable($dict);
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
$rex = '@Loaded Configuration File\s*</td><td class="v">(.*?)</td>@i';
$matches = null;
$ini_loc = null;
if (preg_match($rex, $phpinfo, $matches)) {
$ini_loc = trim($matches[1]);
}
$rex = '@Additional \.ini files parsed\s*</td><td class="v">(.*?)</td>@i';
$more_loc = array();
if (preg_match($rex, $phpinfo, $matches)) {
$more_loc = trim($matches[1]);
if ($more_loc == '(none)') {
$more_loc = array();
} else {
$more_loc = preg_split('/\s*,\s*/', $more_loc);
}
}
$info = array();
if (!$ini_loc) {
$info[] = phutil_tag(
'p',
array(),
pht(
'To update these %d value(s), edit your PHP configuration file.',
count($configs)));
} else {
$info[] = phutil_tag(
'p',
array(),
pht(
'To update these %d value(s), edit your PHP configuration file, '.
'located here:',
count($configs)));
$info[] = phutil_tag(
'pre',
array(),
$ini_loc);
}
if ($more_loc) {
$info[] = phutil_tag(
'p',
array(),
pht(
'PHP also loaded these %s configuration file(s):',
phutil_count($more_loc)));
$info[] = phutil_tag(
'pre',
array(),
implode("\n", $more_loc));
}
$show_standard = false;
$show_opcache = false;
foreach ($configs as $key) {
if (preg_match('/^opcache\./', $key)) {
$show_opcache = true;
} else {
$show_standard = true;
}
}
if ($show_standard) {
$info[] = phutil_tag(
'p',
array(),
pht(
'You can find more information about PHP configuration values '.
'in the %s.',
phutil_tag(
'a',
array(
'href' => 'http://php.net/manual/ini.list.php',
'target' => '_blank',
),
pht('PHP Documentation'))));
}
if ($show_opcache) {
$info[] = phutil_tag(
'p',
array(),
pht(
- 'You can find more information about configuring OPCache in '.
+ 'You can find more information about configuring OPcache in '.
'the %s.',
phutil_tag(
'a',
array(
'href' => 'http://php.net/manual/opcache.configuration.php',
'target' => '_blank',
),
- pht('PHP OPCache Documentation'))));
+ pht('PHP OPcache Documentation'))));
}
$info[] = phutil_tag(
'p',
array(),
pht(
'After editing the PHP configuration, <strong>restart Phabricator for '.
'the changes to take effect</strong>. For help with restarting '.
'Phabricator, see %s in the documentation.',
$this->renderRestartLink()));
return phutil_tag(
'div',
array(
'class' => 'setup-issue-config',
),
array(
$table_info,
$table,
$info,
));
}
private function renderMySQLConfig(array $config) {
$values = array();
$issue = $this->getIssue();
$ref = $issue->getDatabaseRef();
if ($ref) {
foreach ($config as $key) {
$value = $ref->loadRawMySQLConfigValue($key);
if ($value === null) {
$value = phutil_tag(
'em',
array(),
pht('(Not Supported)'));
}
$values[$key] = $value;
}
}
$table = $this->renderValueTable($values);
$doc_href = PhabricatorEnv::getDoclink('User Guide: Amazon RDS');
$doc_link = phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('User Guide: Amazon RDS'));
$info = array();
$info[] = phutil_tag(
'p',
array(),
pht(
'If you are using Amazon RDS, some of the instructions above may '.
'not apply to you. See %s for discussion of Amazon RDS.',
$doc_link));
$table_info = phutil_tag(
'p',
array(),
pht(
'The current MySQL configuration has these %d value(s):',
count($config)));
return phutil_tag(
'div',
array(
'class' => 'setup-issue-config',
),
array(
$table_info,
$table,
$info,
));
}
private function renderValueTable(array $dict, array $hidden = array()) {
$rows = array();
foreach ($dict as $key => $value) {
if (isset($hidden[$key])) {
$value = phutil_tag('em', array(), 'hidden');
} else {
$value = $this->renderValueForDisplay($value);
}
$cols = array(
phutil_tag('th', array(), $key),
phutil_tag('td', array(), $value),
);
$rows[] = phutil_tag('tr', array(), $cols);
}
return phutil_tag('table', array(), $rows);
}
private function renderValueForDisplay($value) {
if ($value === null) {
return phutil_tag('em', array(), 'null');
} else if ($value === false) {
return phutil_tag('em', array(), 'false');
} else if ($value === true) {
return phutil_tag('em', array(), 'true');
} else if ($value === '') {
return phutil_tag('em', array(), 'empty string');
} else if ($value instanceof PhutilSafeHTML) {
return $value;
} else {
return PhabricatorConfigJSON::prettyPrintJSON($value);
}
}
private function renderRelatedLinks(array $links) {
$link_info = phutil_tag(
'p',
array(),
pht(
'%d related link(s):',
count($links)));
$link_list = array();
foreach ($links as $link) {
$link_tag = phutil_tag(
'a',
array(
'target' => '_blank',
'href' => $link['href'],
),
$link['name']);
$link_item = phutil_tag('li', array(), $link_tag);
$link_list[] = $link_item;
}
$link_list = phutil_tag('ul', array(), $link_list);
return phutil_tag(
'div',
array(
'class' => 'setup-issue-config',
),
array(
$link_info,
$link_list,
));
}
private function renderRestartLink() {
$doc_href = PhabricatorEnv::getDoclink('Restarting Phabricator');
return phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Restarting Phabricator'));
}
}
diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php
index 3d451e78d..3b4ad09e1 100644
--- a/src/applications/conpherence/editor/ConpherenceEditor.php
+++ b/src/applications/conpherence/editor/ConpherenceEditor.php
@@ -1,292 +1,249 @@
<?php
final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
const ERROR_EMPTY_PARTICIPANTS = 'error-empty-participants';
const ERROR_EMPTY_MESSAGE = 'error-empty-message';
public function getEditorApplicationClass() {
return 'PhabricatorConpherenceApplication';
}
public function getEditorObjectsDescription() {
return pht('Conpherence Rooms');
}
public static function createThread(
PhabricatorUser $creator,
array $participant_phids,
$title,
$message,
PhabricatorContentSource $source,
$topic) {
$conpherence = ConpherenceThread::initializeNewRoom($creator);
$errors = array();
if (empty($participant_phids)) {
$errors[] = self::ERROR_EMPTY_PARTICIPANTS;
} else {
$participant_phids[] = $creator->getPHID();
$participant_phids = array_unique($participant_phids);
}
if (empty($message)) {
$errors[] = self::ERROR_EMPTY_MESSAGE;
}
if (!$errors) {
$xactions = array();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE)
->setNewValue(array('+' => $participant_phids));
if ($title) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceThreadTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title);
}
if (strlen($topic)) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceThreadTopicTransaction::TRANSACTIONTYPE)
->setNewValue($topic);
}
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ConpherenceTransactionComment())
->setContent($message)
->setConpherencePHID($conpherence->getPHID()));
id(new ConpherenceEditor())
->setActor($creator)
->setContentSource($source)
->setContinueOnNoEffect(true)
->applyTransactions($conpherence, $xactions);
}
return array($errors, $conpherence);
}
public function generateTransactionsFromText(
PhabricatorUser $viewer,
ConpherenceThread $conpherence,
$text) {
$xactions = array();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ConpherenceTransactionComment())
->setContent($text)
->setConpherencePHID($conpherence->getPHID()));
return $xactions;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this room.', $author);
}
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$object->setMessageCount((int)$object->getMessageCount() + 1);
break;
}
return parent::applyBuiltinInternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$acting_phid = $this->getActingAsPHID();
$participants = $object->getParticipants();
foreach ($participants as $participant) {
if ($participant->getParticipantPHID() == $acting_phid) {
$participant->markUpToDate($object);
}
}
if ($participants) {
PhabricatorUserCache::clearCaches(
PhabricatorUserMessageCountCacheType::KEY_COUNT,
array_keys($participants));
}
if ($xactions) {
$data = array(
'type' => 'message',
'threadPHID' => $object->getPHID(),
'messageID' => last($xactions)->getID(),
'subscribers' => array($object->getPHID()),
);
PhabricatorNotificationClient::tryToPostMessage($data);
}
return $xactions;
}
- protected function requireCapabilities(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- parent::requireCapabilities($object, $xaction);
-
- switch ($xaction->getTransactionType()) {
- case ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE:
- $old_map = array_fuse($xaction->getOldValue());
- $new_map = array_fuse($xaction->getNewValue());
-
- $add = array_keys(array_diff_key($new_map, $old_map));
- $rem = array_keys(array_diff_key($old_map, $new_map));
-
- $actor_phid = $this->getActingAsPHID();
-
- $is_join = (($add === array($actor_phid)) && !$rem);
- $is_leave = (($rem === array($actor_phid)) && !$add);
-
- if ($is_join) {
- // Anyone can join a thread they can see.
- } else if ($is_leave) {
- // Anyone can leave a thread.
- } else {
- // You need CAN_EDIT to add or remove participants. For additional
- // discussion, see D17696 and T4411.
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- }
- break;
-
- case ConpherenceThreadTitleTransaction::TRANSACTIONTYPE:
- case ConpherenceThreadTopicTransaction::TRANSACTIONTYPE:
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- break;
- }
- }
-
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ConpherenceReplyHandler())
->setActor($this->getActor())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
if (!$title) {
$title = pht(
'%s sent you a message.',
$this->getActor()->getUserName());
}
return id(new PhabricatorMetaMTAMail())
->setSubject("Z{$id}: {$title}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$to_phids = array();
$participants = $object->getParticipants();
if (!$participants) {
return $to_phids;
}
$participant_phids = mpull($participants, 'getParticipantPHID');
$users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($participant_phids)
->needUserSettings(true)
->execute();
$users = mpull($users, null, 'getPHID');
$notification_key = PhabricatorConpherenceNotificationsSetting::SETTINGKEY;
$notification_email =
PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL;
foreach ($participants as $phid => $participant) {
$user = idx($users, $phid);
if ($user) {
$default = $user->getUserSetting($notification_key);
} else {
$default = $notification_email;
}
$settings = $participant->getSettings();
$notifications = idx($settings, 'notifications', $default);
if ($notifications == $notification_email) {
$to_phids[] = $phid;
}
}
return $to_phids;
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('CONPHERENCE DETAIL'),
PhabricatorEnv::getProductionURI('/'.$object->getMonogram()));
return $body;
}
protected function addEmailPreferenceSectionToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
$href = PhabricatorEnv::getProductionURI(
'/'.$object->getMonogram().'?settings');
$label = pht('EMAIL PREFERENCES FOR THIS ROOM');
$body->addLinkSection($label, $href);
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.conpherence.subject-prefix');
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php b/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php
index 681c6d639..c0e4c7c50 100644
--- a/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php
+++ b/src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php
@@ -1,117 +1,147 @@
<?php
final class ConpherenceThreadParticipantsTransaction
extends ConpherenceThreadTransactionType {
const TRANSACTIONTYPE = 'participants';
public function generateOldValue($object) {
return $object->getParticipantPHIDs();
}
public function generateNewValue($object, $value) {
$old = $this->generateOldValue($object);
return $this->getPHIDList($old, $value);
}
public function applyExternalEffects($object, $value) {
$participants = $object->getParticipants();
$old = array_keys($participants);
$new = $value;
$add_map = array_fuse(array_diff($new, $old));
$rem_map = array_fuse(array_diff($old, $new));
foreach ($rem_map as $phid) {
$remove_participant = $participants[$phid];
$remove_participant->delete();
unset($participants[$phid]);
}
foreach ($add_map as $phid) {
if (isset($participants[$phid])) {
continue;
}
$participants[$phid] = id(new ConpherenceParticipant())
->setConpherencePHID($object->getPHID())
->setParticipantPHID($phid)
->setSeenMessageCount(0)
->save();
}
$object->attachParticipants($participants);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
$author_phid = $this->getAuthorPHID();
if ($add && $rem) {
return pht(
'%s edited participant(s), added %d: %s; removed %d: %s.',
$this->renderAuthor(),
count($add),
$this->renderHandleList($add),
count($rem),
$this->renderHandleList($rem));
} else if ((in_array($author_phid, $add)) && (count($add) == 1)) {
return pht(
'%s joined the room.',
$this->renderAuthor());
} else if ((in_array($author_phid, $rem)) && (count($rem) == 1)) {
return pht(
'%s left the room.',
$this->renderAuthor());
} else if ($add) {
return pht(
'%s added %d participant(s): %s.',
$this->renderAuthor(),
count($add),
$this->renderHandleList($add));
} else {
return pht(
'%s removed %d participant(s): %s.',
$this->renderAuthor(),
count($rem),
$this->renderHandleList($rem));
}
}
public function validateTransactions($object, array $xactions) {
$errors = array();
foreach ($xactions as $xaction) {
$old = $object->getParticipantPHIDs();
$new = $xaction->getNewValue();
$new = $this->getPHIDList($old, $new);
$add_map = array_fuse(array_diff($new, $old));
$rem_map = array_fuse(array_diff($old, $new));
foreach ($add_map as $user_phid) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs(array($user_phid))
->executeOne();
if (!$user) {
$errors[] = $this->newInvalidError(
pht(
'Participant PHID "%s" is not a valid user PHID.',
$user_phid));
continue;
}
}
}
return $errors;
}
+ public function getRequiredCapabilities(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+
+ $old_map = array_fuse($xaction->getOldValue());
+ $new_map = array_fuse($xaction->getNewValue());
+
+ $add = array_keys(array_diff_key($new_map, $old_map));
+ $rem = array_keys(array_diff_key($old_map, $new_map));
+
+ $actor_phid = $this->getActingAsPHID();
+
+ $is_join = (($add === array($actor_phid)) && !$rem);
+ $is_leave = (($rem === array($actor_phid)) && !$add);
+
+ if ($is_join) {
+ // Anyone can join a thread they can see.
+ return null;
+ }
+
+ if ($is_leave) {
+ // Anyone can leave a thread.
+ return null;
+ }
+
+ // You need CAN_EDIT to add or remove participants. For additional
+ // discussion, see D17696 and T4411.
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ }
+
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php
index fd3e61d86..9a8b80738 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php
@@ -1,170 +1,170 @@
<?php
final class PhabricatorDaemonBulkJobMonitorController
extends PhabricatorDaemonBulkJobController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$job = id(new PhabricatorWorkerBulkJobQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$job) {
return new Aphront404Response();
}
// If the user clicks "Continue" on a completed job, take them back to
// whatever application sent them here.
if ($request->getStr('done')) {
if ($request->isFormPost()) {
$done_uri = $job->getDoneURI();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
$title = pht('Bulk Job %d', $job->getID());
if ($job->getStatus() == PhabricatorWorkerBulkJob::STATUS_CONFIRM) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$job,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
if ($request->isFormPost()) {
$type_status = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
$xactions = array();
$xactions[] = id(new PhabricatorWorkerBulkJobTransaction())
->setTransactionType($type_status)
->setNewValue(PhabricatorWorkerBulkJob::STATUS_WAITING);
$editor = id(new PhabricatorWorkerBulkJobEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->applyTransactions($job, $xactions);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
} else {
$dialog = $this->newDialog()
->setTitle(pht('Confirm Bulk Job'));
$confirm = $job->getDescriptionForConfirm();
$confirm = (array)$confirm;
foreach ($confirm as $paragraph) {
$dialog->appendParagraph($paragraph);
}
$dialog
->appendParagraph(
pht('Start work on this bulk job?'))
->addCancelButton($job->getManageURI(), pht('Details'))
->addSubmitButton(pht('Start Work'));
return $dialog;
}
} else {
return $this->newDialog()
->setTitle(pht('Waiting For Confirmation'))
->appendParagraph(
pht(
'This job is waiting for confirmation before work begins.'))
- ->addCancelButotn($job->getManageURI(), pht('Details'));
+ ->addCancelButton($job->getManageURI(), pht('Details'));
}
}
$dialog = $this->newDialog()
->setTitle(pht('%s: %s', $title, $job->getStatusName()))
->addCancelButton($job->getManageURI(), pht('Details'));
switch ($job->getStatus()) {
case PhabricatorWorkerBulkJob::STATUS_WAITING:
$dialog->appendParagraph(
pht('This job is waiting for tasks to be queued.'));
break;
case PhabricatorWorkerBulkJob::STATUS_RUNNING:
$dialog->appendParagraph(
pht('This job is running.'));
break;
case PhabricatorWorkerBulkJob::STATUS_COMPLETE:
$dialog->appendParagraph(
pht('This job is complete.'));
break;
}
$counts = $job->loadTaskStatusCounts();
if ($counts) {
$dialog->appendParagraph($this->renderProgress($counts));
}
switch ($job->getStatus()) {
case PhabricatorWorkerBulkJob::STATUS_COMPLETE:
$dialog->addHiddenInput('done', true);
$dialog->addSubmitButton(pht('Continue'));
break;
default:
Javelin::initBehavior('bulk-job-reload');
break;
}
return $dialog;
}
private function renderProgress(array $counts) {
$this->requireResource('bulk-job-css');
$states = array(
PhabricatorWorkerBulkTask::STATUS_DONE => array(
'class' => 'bulk-job-progress-slice-green',
),
PhabricatorWorkerBulkTask::STATUS_RUNNING => array(
'class' => 'bulk-job-progress-slice-blue',
),
PhabricatorWorkerBulkTask::STATUS_WAITING => array(
'class' => 'bulk-job-progress-slice-empty',
),
PhabricatorWorkerBulkTask::STATUS_FAIL => array(
'class' => 'bulk-job-progress-slice-red',
),
);
$total = array_sum($counts);
$offset = 0;
$bars = array();
foreach ($states as $state => $spec) {
$size = idx($counts, $state, 0);
if (!$size) {
continue;
}
$classes = array();
$classes[] = 'bulk-job-progress-slice';
$classes[] = $spec['class'];
$width = ($size / $total);
$bars[] = phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'style' =>
'left: '.sprintf('%.2f%%', 100 * $offset).'; '.
'width: '.sprintf('%.2f%%', 100 * $width).';',
),
'');
$offset += $width;
}
return phutil_tag(
'div',
array(
'class' => 'bulk-job-progress-bar',
),
$bars);
}
}
diff --git a/src/applications/dashboard/controller/PhabricatorDashboardProfileController.php b/src/applications/dashboard/controller/PhabricatorDashboardProfileController.php
index 6a23b351a..adc85efdf 100644
--- a/src/applications/dashboard/controller/PhabricatorDashboardProfileController.php
+++ b/src/applications/dashboard/controller/PhabricatorDashboardProfileController.php
@@ -1,95 +1,98 @@
<?php
abstract class PhabricatorDashboardProfileController
extends PhabricatorController {
private $dashboard;
public function setDashboard(PhabricatorDashboard $dashboard) {
$this->dashboard = $dashboard;
return $this;
}
public function getDashboard() {
return $this->dashboard;
}
public function buildApplicationMenu() {
return $this->buildSideNavView()->getMenu();
}
protected function buildHeaderView() {
$viewer = $this->getViewer();
$dashboard = $this->getDashboard();
$id = $dashboard->getID();
if ($dashboard->isArchived()) {
$status_icon = 'fa-ban';
$status_color = 'dark';
} else {
$status_icon = 'fa-check';
$status_color = 'bluegrey';
}
$status_name = idx(
PhabricatorDashboard::getStatusNameMap(),
$dashboard->getStatus());
return id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($dashboard->getName())
->setPolicyObject($dashboard)
->setStatus($status_icon, $status_color, $status_name)
->setHeaderIcon($dashboard->getIcon());
}
protected function buildApplicationCrumbs() {
- $dashboard = $this->getDashboard();
- $id = $dashboard->getID();
- $dashboard_uri = $this->getApplicationURI("/view/{$id}/");
-
$crumbs = parent::buildApplicationCrumbs();
- $crumbs->addTextCrumb($dashboard->getName(), $dashboard_uri);
$crumbs->setBorder(true);
+
+ $dashboard = $this->getDashboard();
+ if ($dashboard) {
+ $id = $dashboard->getID();
+ $dashboard_uri = $this->getApplicationURI("/view/{$id}/");
+ $crumbs->addTextCrumb($dashboard->getName(), $dashboard_uri);
+ }
+
return $crumbs;
}
protected function buildSideNavView($filter = null) {
$viewer = $this->getViewer();
$dashboard = $this->getDashboard();
$id = $dashboard->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$dashboard,
PhabricatorPolicyCapability::CAN_EDIT);
$nav = id(new AphrontSideNavFilterView())
->setBaseURI(new PhutilURI($this->getApplicationURI()));
$nav->addLabel(pht('Dashboard'));
$nav->addFilter(
'view',
pht('View Dashboard'),
$this->getApplicationURI("/view/{$id}/"),
'fa-dashboard');
$nav->addFilter(
'arrange',
pht('Arrange Panels'),
$this->getApplicationURI("/arrange/{$id}/"),
'fa-columns');
$nav->addFilter(
'manage',
pht('Manage Dashboard'),
$this->getApplicationURI("/manage/{$id}/"),
'fa-gears');
$nav->selectFilter($filter);
return $nav;
}
}
diff --git a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
index 8bbdb1a5e..ac80288b9 100644
--- a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
+++ b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php
@@ -1,66 +1,77 @@
<?php
final class PhabricatorDashboardPanelDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Dashboard Panels');
}
public function getPlaceholderText() {
return pht('Type a panel name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDashboardApplication';
}
public function loadResults() {
$results = $this->buildResults();
return $this->filterResultsAgainstTokens($results);
}
-
protected function renderSpecialTokens(array $values) {
return $this->renderTokensFromResults($this->buildResults(), $values);
}
+ public function buildResults() {
+ $query = new PhabricatorDashboardPanelQuery();
+
+ $raw_query = $this->getRawQuery();
+ if (preg_match('/^[wW]\d+\z/', $raw_query)) {
+ $id = trim($raw_query, 'wW');
+ $id = (int)$id;
+ $query->withIDs(array($id));
+ } else {
+ $query->withNameNgrams($raw_query);
+ }
+
public function buildResults() {
$query = id(new PhabricatorDashboardPanelQuery());
$panels = $this->executeQuery($query);
$results = array();
foreach ($panels as $panel) {
$impl = $panel->getImplementation();
if ($impl) {
$type_text = $impl->getPanelTypeName();
$icon = $impl->getIcon();
} else {
$type_text = nonempty($panel->getPanelType(), pht('Unknown Type'));
$icon = 'fa-question';
}
$id = $panel->getID();
$monogram = $panel->getMonogram();
$properties = $panel->getProperties();
$result = id(new PhabricatorTypeaheadResult())
->setName($monogram.' '.$panel->getName())
->setPHID($id)
->setIcon($icon)
->addAttribute($type_text);
if (!empty($properties['class'])) {
$result->addAttribute($properties['class']);
}
if ($panel->getIsArchived()) {
$result->setClosed(pht('Archived'));
}
$results[$id] = $result;
}
return $results;
}
}
diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php
index eaa4948ee..52b222301 100644
--- a/src/applications/differential/application/PhabricatorDifferentialApplication.php
+++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php
@@ -1,146 +1,149 @@
<?php
final class PhabricatorDifferentialApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/differential/';
}
public function getName() {
return pht('Differential');
}
public function getShortDescription() {
return pht('Pre-Commit Review');
}
public function getIcon() {
return 'fa-cog';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Differential User Guide'),
'href' => PhabricatorEnv::getDoclink('Differential User Guide'),
),
);
}
public function getTitleGlyph() {
return "\xE2\x9A\x99";
}
public function getOverview() {
return pht(
'Differential is a **code review application** which allows '.
'engineers to review, discuss and approve changes to software.');
}
public function getRoutes() {
return array(
- '/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController',
+ '/D(?P<id>[1-9]\d*)' => array(
+ '' => 'DifferentialRevisionViewController',
+ '/(?P<filter>new)/' => 'DifferentialRevisionViewController',
+ ),
'/differential/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'DifferentialRevisionListController',
'diff/' => array(
'(?P<id>[1-9]\d*)/' => array(
'' => 'DifferentialDiffViewController',
'changesets/' => array(
$this->getQueryRoutePattern()
=> 'DifferentialChangesetListController',
),
),
'create/' => 'DifferentialDiffCreateController',
),
'changeset/' => 'DifferentialChangesetViewController',
'revision/' => array(
$this->getEditRoutePattern('edit/')
=> 'DifferentialRevisionEditController',
$this->getEditRoutePattern('attach/(?P<diffID>[^/]+)/to/')
=> 'DifferentialRevisionEditController',
'closedetails/(?P<phid>[^/]+)/'
=> 'DifferentialRevisionCloseDetailsController',
'update/(?P<revisionID>[1-9]\d*)/'
=> 'DifferentialDiffCreateController',
'operation/(?P<id>[1-9]\d*)/'
=> 'DifferentialRevisionOperationController',
'inlines/(?P<id>[1-9]\d*)/'
=> 'DifferentialRevisionInlinesController',
),
'comment/' => array(
'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',
'save/(?P<id>[1-9]\d*)/' => 'DifferentialCommentSaveController',
'inline/' => array(
'preview/(?P<id>[1-9]\d*)/'
=> 'DifferentialInlineCommentPreviewController',
'edit/(?P<id>[1-9]\d*)/'
=> 'DifferentialInlineCommentEditController',
),
),
'preview/' => 'PhabricatorMarkupPreviewController',
),
);
}
public function getApplicationOrder() {
return 0.100;
}
public function getRemarkupRules() {
return array(
new DifferentialRemarkupRule(),
);
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send email to these addresses to create revisions. The body of the '.
'message and / or one or more attachments should be the output of a '.
'"diff" command. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
protected function getCustomCapabilities() {
return array(
DifferentialDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created revisions.'),
'template' => DifferentialRevisionPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
);
}
public function getMailCommandObjects() {
return array(
'revision' => array(
'name' => pht('Email Commands: Revisions'),
'header' => pht('Interacting with Differential Revisions'),
'object' => new DifferentialRevision(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'revisions in Differential.'),
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
DifferentialRevisionPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
index 9d02202f9..c41f95139 100644
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -1,455 +1,456 @@
<?php
final class DifferentialChangesetViewController extends DifferentialController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$rendering_reference = $request->getStr('ref');
$parts = explode('/', $rendering_reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
$load_ids = array($id);
if ($vs && ($vs != -1)) {
$load_ids[] = $vs;
}
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs($load_ids)
->needHunks(true)
->execute();
$changesets = mpull($changesets, null, 'getID');
$changeset = idx($changesets, $id);
if (!$changeset) {
return new Aphront404Response();
}
$vs_changeset = null;
if ($vs && ($vs != -1)) {
$vs_changeset = idx($changesets, $vs);
if (!$vs_changeset) {
return new Aphront404Response();
}
}
$view = $request->getStr('view');
if ($view) {
$phid = idx($changeset->getMetadata(), "$view:binary-phid");
if ($phid) {
return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/");
}
switch ($view) {
case 'new':
return $this->buildRawFileResponse($changeset, $is_new = true);
case 'old':
if ($vs_changeset) {
return $this->buildRawFileResponse($vs_changeset, $is_new = true);
}
return $this->buildRawFileResponse($changeset, $is_new = false);
default:
return new Aphront400Response();
}
}
$old = array();
$new = array();
if (!$vs) {
$right = $changeset;
$left = null;
$right_source = $right->getID();
$right_new = true;
$left_source = $right->getID();
$left_new = false;
$render_cache_key = $right->getID();
$old[] = $changeset;
$new[] = $changeset;
} else if ($vs == -1) {
$right = null;
$left = $changeset;
$right_source = $left->getID();
$right_new = false;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
$old[] = $changeset;
$new[] = $changeset;
} else {
$right = $changeset;
$left = $vs_changeset;
$right_source = $right->getID();
$right_new = true;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
$new[] = $left;
$new[] = $right;
}
if ($left) {
$left_data = $left->makeNewFile();
if ($right) {
$right_data = $right->makeNewFile();
} else {
$right_data = $left->makeOldFile();
}
$engine = new PhabricatorDifferenceEngine();
$synthetic = $engine->generateChangesetFromFileContent(
$left_data,
$right_data);
$choice = clone nonempty($left, $right);
$choice->attachHunks($synthetic->getHunks());
$changeset = $choice;
}
if ($left_new || $right_new) {
$diff_map = array();
if ($left) {
$diff_map[] = $left->getDiff();
}
if ($right) {
$diff_map[] = $right->getDiff();
}
$diff_map = mpull($diff_map, null, 'getPHID');
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array_keys($diff_map))
->withManualBuildables(false)
->needBuilds(true)
->needTargets(true)
->execute();
$buildables = mpull($buildables, null, 'getBuildablePHID');
foreach ($diff_map as $diff_phid => $changeset_diff) {
$changeset_diff->attachBuildable(idx($buildables, $diff_phid));
}
}
$coverage = null;
if ($right_new) {
$coverage = $this->loadCoverage($right);
}
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$parser = id(new DifferentialChangesetParser())
->setCoverage($coverage)
->setChangeset($changeset)
->setRenderingReference($rendering_reference)
->setRenderCacheKey($render_cache_key)
->setRightSideCommentMapping($right_source, $right_new)
->setLeftSideCommentMapping($left_source, $left_new);
$parser->readParametersFromRequest($request);
if ($left && $right) {
$parser->setOriginals($left, $right);
}
$diff = $changeset->getDiff();
$revision_id = $diff->getRevisionID();
$can_mark = false;
$object_owner_phid = null;
$revision = null;
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if ($revision) {
$can_mark = ($revision->getAuthorPHID() == $viewer->getPHID());
$object_owner_phid = $revision->getAuthorPHID();
}
}
// Load both left-side and right-side inline comments.
if ($revision) {
$query = id(new DifferentialInlineCommentQuery())
->setViewer($viewer)
->needHidden(true)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
$inlines = $query->adjustInlinesForChangesets(
$inlines,
$old,
$new,
$revision);
} else {
$inlines = array();
}
if ($left_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($left));
}
if ($right_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($right));
}
$phids = array();
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
if ($inline->getAuthorPHID()) {
$phids[$inline->getAuthorPHID()] = true;
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$parser->setHandles($handles);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$parser
->setUser($viewer)
->setMarkupEngine($engine)
->setShowEditAndReplyLinks(true)
->setCanMarkDone($can_mark)
->setObjectOwnerPHID($object_owner_phid)
->setRange($range_s, $range_e)
->setMask($mask);
if ($request->isAjax()) {
// NOTE: We must render the changeset before we render coverage
// information, since it builds some caches.
$rendered_changeset = $parser->renderChangeset();
$mcov = $parser->renderModifiedCoverage();
$coverage_data = array(
'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov,
);
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($rendered_changeset)
->setCoverage($coverage_data)
->setUndoTemplates($parser->getRenderer()->renderUndoTemplates());
}
$detail = id(new DifferentialChangesetListView())
->setUser($this->getViewer())
->setChangesets(array($changeset))
->setVisibleChangesets(array($changeset))
->setRenderingReferences(array($rendering_reference))
->setRenderURI('/differential/changeset/')
->setDiff($diff)
->setTitle(pht('Standalone View'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setIsStandalone(true)
->setParser($parser);
if ($revision_id) {
$detail->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision_id.'/');
}
$crumbs = $this->buildApplicationCrumbs();
if ($revision_id) {
$crumbs->addTextCrumb('D'.$revision_id, '/D'.$revision_id);
}
$diff_id = $diff->getID();
if ($diff_id) {
$crumbs->addTextCrumb(
pht('Diff %d', $diff_id),
$this->getApplicationURI('diff/'.$diff_id));
}
$crumbs->addTextCrumb($changeset->getDisplayFilename());
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Changeset View'))
->setHeaderIcon('fa-gear');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($detail);
return $this->newPage()
->setTitle(pht('Changeset View'))
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildRawFileResponse(
DifferentialChangeset $changeset,
$is_new) {
$viewer = $this->getViewer();
if ($is_new) {
$key = 'raw:new:phid';
} else {
$key = 'raw:old:phid';
}
$metadata = $changeset->getMetadata();
$file = null;
$phid = idx($metadata, $key);
if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->execute();
if ($file) {
$file = head($file);
}
}
if (!$file) {
// This is just building a cache of the changeset content in the file
// tool, and is safe to run on a read pathway.
$unguard = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($is_new) {
$data = $changeset->makeNewFile();
} else {
$data = $changeset->makeOldFile();
}
$diff = $changeset->getDiff();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $changeset->getFilename(),
'mime-type' => 'text/plain',
'ttl.relative' => phutil_units('24 hours in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file->attachToObject($diff->getPHID());
$metadata[$key] = $file->getPHID();
$changeset->setMetadata($metadata);
$changeset->save();
unset($unguard);
}
return $file->getRedirectResponse();
}
private function buildLintInlineComments($changeset) {
$diff = $changeset->getDiff();
$target_phids = $diff->getBuildTargetPHIDs();
if (!$target_phids) {
return array();
}
$messages = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) AND path = %s',
$target_phids,
$changeset->getFilename());
if (!$messages) {
return array();
}
$change_type = $changeset->getChangeType();
if (DifferentialChangeType::isDeleteChangeType($change_type)) {
// If this is a lint message on a deleted file, show it on the left
// side of the UI because there are no source code lines on the right
// side of the UI so inlines don't have anywhere to render. See PHI416.
$is_new = 0;
} else {
$is_new = 1;
}
$template = id(new DifferentialInlineComment())
->setChangesetID($changeset->getID())
->setIsNewFile($is_new)
->setLineLength(0);
$inlines = array();
foreach ($messages as $message) {
$description = $message->getProperty('description');
$inlines[] = id(clone $template)
->setSyntheticAuthor(pht('Lint: %s', $message->getName()))
->setLineNumber($message->getLine())
->setContent($description);
}
return $inlines;
}
private function loadCoverage(DifferentialChangeset $changeset) {
$target_phids = $changeset->getDiff()->getBuildTargetPHIDs();
if (!$target_phids) {
return null;
}
$unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
if (!$unit) {
return null;
}
$coverage = array();
foreach ($unit as $message) {
$test_coverage = $message->getProperty('coverage');
if ($test_coverage === null) {
continue;
}
$coverage_data = idx($test_coverage, $changeset->getFileName());
if (!strlen($coverage_data)) {
continue;
}
$coverage[] = $coverage_data;
}
if (!$coverage) {
return null;
}
return ArcanistUnitTestResult::mergeCoverage($coverage);
}
}
diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php
index 1860a0774..8fe5b5cac 100644
--- a/src/applications/differential/controller/DifferentialController.php
+++ b/src/applications/differential/controller/DifferentialController.php
@@ -1,276 +1,286 @@
<?php
abstract class DifferentialController extends PhabricatorController {
private $packageChangesetMap;
private $pathPackageMap;
private $authorityPackages;
public function buildSideNavView($for_app = false) {
$viewer = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new DifferentialRevisionSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
public function buildApplicationMenu() {
return $this->buildSideNavView(true)->getMenu();
}
protected function buildPackageMaps(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->packageChangesetMap = array();
$this->pathPackageMap = array();
$this->authorityPackages = array();
if (!$changesets) {
return;
}
$viewer = $this->getViewer();
$have_owners = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorOwnersApplication',
$viewer);
if (!$have_owners) {
return;
}
$changeset = head($changesets);
$diff = $changeset->getDiff();
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return;
}
if ($viewer->getPHID()) {
$packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->execute();
$this->authorityPackages = $packages;
}
$paths = mpull($changesets, 'getOwnersFilename');
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withControl($repository_phid, $paths);
$control_query->execute();
foreach ($changesets as $changeset) {
$changeset_path = $changeset->getOwnersFilename();
$packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$changeset_path);
+ // If this particular changeset is generated code and the package does
+ // not match generated code, remove it from the list.
+ if ($changeset->isGeneratedChangeset()) {
+ foreach ($packages as $key => $package) {
+ if ($package->getMustMatchUngeneratedPaths()) {
+ unset($packages[$key]);
+ }
+ }
+ }
+
$this->pathPackageMap[$changeset_path] = $packages;
foreach ($packages as $package) {
$this->packageChangesetMap[$package->getPHID()][] = $changeset;
}
}
}
protected function getAuthorityPackages() {
if ($this->authorityPackages === null) {
throw new PhutilInvalidStateException('buildPackageMaps');
}
return $this->authorityPackages;
}
protected function getChangesetPackages(DifferentialChangeset $changeset) {
if ($this->pathPackageMap === null) {
throw new PhutilInvalidStateException('buildPackageMaps');
}
$path = $changeset->getOwnersFilename();
return idx($this->pathPackageMap, $path, array());
}
protected function getPackageChangesets($package_phid) {
if ($this->packageChangesetMap === null) {
throw new PhutilInvalidStateException('buildPackageMaps');
}
return idx($this->packageChangesetMap, $package_phid, array());
}
protected function buildTableOfContents(
array $changesets,
array $visible_changesets,
array $coverage) {
$viewer = $this->getViewer();
$toc_view = id(new PHUIDiffTableOfContentsListView())
->setViewer($viewer)
->setBare(true)
->setAuthorityPackages($this->getAuthorityPackages());
foreach ($changesets as $changeset_id => $changeset) {
$is_visible = isset($visible_changesets[$changeset_id]);
$anchor = $changeset->getAnchorName();
$filename = $changeset->getFilename();
$coverage_id = 'differential-mcoverage-'.md5($filename);
$item = id(new PHUIDiffTableOfContentsItemView())
->setChangeset($changeset)
->setIsVisible($is_visible)
->setAnchor($anchor)
->setCoverage(idx($coverage, $filename))
->setCoverageID($coverage_id);
$packages = $this->getChangesetPackages($changeset);
$item->setPackages($packages);
$toc_view->addItem($item);
}
return $toc_view;
}
protected function loadDiffProperties(array $diffs) {
$diffs = mpull($diffs, null, 'getID');
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID IN (%Ld)',
array_keys($diffs));
$properties = mgroup($properties, 'getDiffID');
foreach ($diffs as $id => $diff) {
$values = idx($properties, $id, array());
$values = mpull($values, 'getData', 'getName');
$diff->attachDiffProperties($values);
}
}
protected function loadHarbormasterData(array $diffs) {
$viewer = $this->getViewer();
$diffs = mpull($diffs, null, 'getPHID');
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array_keys($diffs))
->withManualBuildables(false)
->needBuilds(true)
->needTargets(true)
->execute();
$buildables = mpull($buildables, null, 'getBuildablePHID');
foreach ($diffs as $phid => $diff) {
$diff->attachBuildable(idx($buildables, $phid));
}
$target_map = array();
foreach ($diffs as $phid => $diff) {
$target_map[$phid] = $diff->getBuildTargetPHIDs();
}
$all_target_phids = array_mergev($target_map);
if ($all_target_phids) {
$unit_messages = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$all_target_phids);
$unit_messages = mgroup($unit_messages, 'getBuildTargetPHID');
} else {
$unit_messages = array();
}
foreach ($diffs as $phid => $diff) {
$target_phids = idx($target_map, $phid, array());
$messages = array_select_keys($unit_messages, $target_phids);
$messages = array_mergev($messages);
$diff->attachUnitMessages($messages);
}
// For diffs with no messages, look for legacy unit messages stored on the
// diff itself.
foreach ($diffs as $phid => $diff) {
if ($diff->getUnitMessages()) {
continue;
}
if (!$diff->hasDiffProperty('arc:unit')) {
continue;
}
$legacy_messages = $diff->getProperty('arc:unit');
if (!$legacy_messages) {
continue;
}
// Show the top 100 legacy lint messages. Previously, we showed some
// by default and let the user toggle the rest. With modern messages,
// we can send the user to the Harbormaster detail page. Just show
// "a lot" of messages in legacy cases to try to strike a balance
// between implementation simplicity and compatibility.
$legacy_messages = array_slice($legacy_messages, 0, 100);
$messages = array();
foreach ($legacy_messages as $message) {
$messages[] = HarbormasterBuildUnitMessage::newFromDictionary(
new HarbormasterBuildTarget(),
$this->getModernUnitMessageDictionary($message));
}
$diff->attachUnitMessages($messages);
}
}
private function getModernUnitMessageDictionary(array $map) {
// Strip out `null` values to satisfy stricter typechecks.
foreach ($map as $key => $value) {
if ($value === null) {
unset($map[$key]);
}
}
// Cast duration to a float since it used to be a string in some
// cases.
if (isset($map['duration'])) {
$map['duration'] = (double)$map['duration'];
}
return $map;
}
protected function getDiffTabLabels(array $diffs) {
// Make sure we're only going to render unique diffs.
$diffs = mpull($diffs, null, 'getID');
$labels = array(pht('Left'), pht('Right'));
$results = array();
foreach ($diffs as $diff) {
if (count($diffs) == 2) {
$label = array_shift($labels);
$label = pht('%s (Diff %d)', $label, $diff->getID());
} else {
$label = pht('Diff %d', $diff->getID());
}
$results[] = array(
$label,
$diff,
);
}
return $results;
}
}
diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
index 926158cb4..9741cc93e 100644
--- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php
+++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
@@ -1,224 +1,235 @@
<?php
final class DifferentialInlineCommentEditController
extends PhabricatorInlineCommentController {
private function getRevisionID() {
return $this->getRequest()->getURIData('id');
}
private function loadRevision() {
$viewer = $this->getViewer();
$revision_id = $this->getRevisionID();
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if (!$revision) {
throw new Exception(pht('Invalid revision ID "%s".', $revision_id));
}
return $revision;
}
protected function createComment() {
// Verify revision and changeset correspond to actual objects, and are
// connected to one another.
$changeset_id = $this->getChangesetID();
$viewer = $this->getViewer();
$revision = $this->loadRevision();
$changeset = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs(array($changeset_id))
->executeOne();
if (!$changeset) {
throw new Exception(
pht(
'Invalid changeset ID "%s"!',
$changeset_id));
}
$diff = $changeset->getDiff();
if ($diff->getRevisionID() != $revision->getID()) {
throw new Exception(
pht(
'Changeset ID "%s" is part of diff ID "%s", but that diff '.
'is attached to revision "%s", not revision "%s".',
$changeset_id,
$diff->getID(),
$diff->getRevisionID(),
$revision->getID()));
}
return id(new DifferentialInlineComment())
->setRevision($revision)
->setChangesetID($changeset_id);
}
protected function loadComment($id) {
return id(new DifferentialInlineCommentQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->withDeletedDrafts(true)
->needHidden(true)
->executeOne();
}
protected function loadCommentByPHID($phid) {
return id(new DifferentialInlineCommentQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->withDeletedDrafts(true)
->needHidden(true)
->executeOne();
}
protected function loadCommentForEdit($id) {
- $request = $this->getRequest();
- $user = $request->getUser();
+ $viewer = $this->getViewer();
$inline = $this->loadComment($id);
- if (!$this->canEditInlineComment($user, $inline)) {
+ if (!$this->canEditInlineComment($viewer, $inline)) {
throw new Exception(pht('That comment is not editable!'));
}
return $inline;
}
protected function loadCommentForDone($id) {
- $request = $this->getRequest();
- $viewer = $request->getUser();
+ $viewer = $this->getViewer();
$inline = $this->loadComment($id);
if (!$inline) {
throw new Exception(pht('Unable to load inline "%d".', $id));
}
$changeset = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs(array($inline->getChangesetID()))
->executeOne();
if (!$changeset) {
throw new Exception(pht('Unable to load changeset.'));
}
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($changeset->getDiffID()))
->executeOne();
if (!$diff) {
throw new Exception(pht('Unable to load diff.'));
}
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($diff->getRevisionID()))
->executeOne();
if (!$revision) {
throw new Exception(pht('Unable to load revision.'));
}
- if ($revision->getAuthorPHID() !== $viewer->getPHID()) {
- throw new Exception(pht('You are not the revision owner.'));
+ $viewer_phid = $viewer->getPHID();
+ $is_owner = ($viewer_phid == $revision->getAuthorPHID());
+ $is_author = ($viewer_phid == $inline->getAuthorPHID());
+ $is_draft = ($inline->isDraft());
+
+ if ($is_owner) {
+ // You own the revision, so you can mark the comment as "Done".
+ } else if ($is_author && $is_draft) {
+ // You made this comment and it's still a draft, so you can mark
+ // it as "Done".
+ } else {
+ throw new Exception(
+ pht(
+ 'You are not the revision owner, and this is not a draft comment '.
+ 'you authored.'));
}
return $inline;
}
private function canEditInlineComment(
- PhabricatorUser $user,
+ PhabricatorUser $viewer,
DifferentialInlineComment $inline) {
// Only the author may edit a comment.
- if ($inline->getAuthorPHID() != $user->getPHID()) {
+ if ($inline->getAuthorPHID() != $viewer->getPHID()) {
return false;
}
// Saved comments may not be edited, for now, although the schema now
// supports it.
if (!$inline->isDraft()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getRevisionID() != $this->getRevisionID()) {
return false;
}
return true;
}
protected function deleteComment(PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->setIsDeleted(1)->save();
$this->syncDraft();
$inline->saveTransaction();
}
protected function undeleteComment(
PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->setIsDeleted(0)->save();
$this->syncDraft();
$inline->saveTransaction();
}
protected function saveComment(PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->save();
$this->syncDraft();
$inline->saveTransaction();
}
protected function loadObjectOwnerPHID(
PhabricatorInlineCommentInterface $inline) {
return $this->loadRevision()->getAuthorPHID();
}
protected function hideComments(array $ids) {
$viewer = $this->getViewer();
$table = new DifferentialHiddenComment();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$viewer->getPHID(),
$id);
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %Q',
$table->getTableName(),
implode(', ', $sql));
}
protected function showComments(array $ids) {
$viewer = $this->getViewer();
$table = new DifferentialHiddenComment();
$conn_w = $table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE userPHID = %s AND commentID IN (%Ld)',
$table->getTableName(),
$viewer->getPHID(),
$ids);
}
private function syncDraft() {
$viewer = $this->getViewer();
$revision = $this->loadRevision();
$revision->newDraftEngine()
->setObject($revision)
->setViewer($viewer)
->synchronize();
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index 9b015887a..880582ad8 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1,1213 +1,1458 @@
<?php
-final class DifferentialRevisionViewController extends DifferentialController {
+final class DifferentialRevisionViewController
+ extends DifferentialController {
private $revisionID;
- private $veryLargeDiff;
+ private $changesetCount;
+ private $hiddenChangesets;
+ private $warnings = array();
public function shouldAllowPublic() {
return true;
}
+ public function isLargeDiff() {
+ return ($this->getChangesetCount() > $this->getLargeDiffLimit());
+ }
+
public function isVeryLargeDiff() {
- return $this->veryLargeDiff;
+ return ($this->getChangesetCount() > $this->getVeryLargeDiffLimit());
+ }
+
+ public function getLargeDiffLimit() {
+ return 100;
}
public function getVeryLargeDiffLimit() {
return 1000;
}
+ public function getChangesetCount() {
+ if ($this->changesetCount === null) {
+ throw new PhutilInvalidStateException('setChangesetCount');
+ }
+ return $this->changesetCount;
+ }
+
+ public function setChangesetCount($count) {
+ $this->changesetCount = $count;
+ return $this;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->revisionID = $request->getURIData('id');
$viewer_is_anonymous = !$viewer->isLoggedIn();
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($this->revisionID))
->setViewer($viewer)
->needReviewers(true)
->needReviewerAuthority(true)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
$diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withRevisionIDs(array($this->revisionID))
->execute();
$diffs = array_reverse($diffs, $preserve_keys = true);
if (!$diffs) {
throw new Exception(
pht('This revision has no diffs. Something has gone quite wrong.'));
}
$revision->attachActiveDiff(last($diffs));
- $diff_vs = $request->getInt('vs');
- $target_id = $request->getInt('id');
- $target = idx($diffs, $target_id, end($diffs));
+ $diff_vs = $this->getOldDiffID($revision, $diffs);
+ if ($diff_vs instanceof AphrontResponse) {
+ return $diff_vs;
+ }
+
+ $target_id = $this->getNewDiffID($revision, $diffs);
+ if ($target_id instanceof AphrontResponse) {
+ return $target_id;
+ }
+
+ $target = $diffs[$target_id];
$target_manual = $target;
if (!$target_id) {
foreach ($diffs as $diff) {
if ($diff->getCreationMethod() != 'commit') {
$target_manual = $diff;
}
}
}
- 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($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
}
}
list($changesets, $vs_map, $vs_changesets, $rendering_references) =
$this->loadChangesetsAndVsMap(
$target,
idx($diffs, $diff_vs),
$repository);
- if (count($rendering_references) > $this->getVeryLargeDiffLimit()) {
- $this->veryLargeDiff = true;
- }
+ $this->setChangesetCount(count($rendering_references));
if ($request->getExists('download')) {
return $this->buildRawDiffResponse(
$revision,
$changesets,
$vs_changesets,
$vs_map,
$repository);
}
$map = $vs_map;
if (!$map) {
$map = array_fill_keys(array_keys($changesets), 0);
}
$old_ids = array();
$new_ids = array();
foreach ($map as $id => $vs) {
if ($vs <= 0) {
$old_ids[] = $id;
$new_ids[] = $id;
} else {
$new_ids[] = $id;
$new_ids[] = $vs;
}
}
$this->loadDiffProperties($diffs);
$props = $target_manual->getDiffProperties();
$subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$revision->getPHID());
$object_phids = array_merge(
$revision->getReviewerPHIDs(),
$subscriber_phids,
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$viewer->getPHID(),
));
foreach ($revision->getAttached() as $type => $phids) {
foreach ($phids as $phid => $info) {
$object_phids[] = $phid;
}
}
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_VIEW);
$field_list->setViewer($viewer);
$field_list->readFieldsFromStorage($revision);
$warning_handle_map = array();
foreach ($field_list->getFields() as $key => $field) {
$req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings();
foreach ($req as $phid) {
$warning_handle_map[$key][] = $phid;
$object_phids[] = $phid;
}
}
$handles = $this->loadViewerHandles($object_phids);
+ $warnings = $this->warnings;
$request_uri = $request->getRequestURI();
- $limit = 100;
$large = $request->getStr('large');
- if (count($changesets) > $limit && !$large) {
- $count = count($changesets);
- $warning = new PHUIInfoView();
- $warning->setTitle(pht('Large Diff'));
- $warning->setSeverity(PHUIInfoView::SEVERITY_WARNING);
- $warning->appendChild(hsprintf(
- '%s <strong>%s</strong>',
+
+ $large_warning =
+ ($this->isLargeDiff()) &&
+ (!$this->isVeryLargeDiff()) &&
+ (!$large);
+
+ if ($large_warning) {
+ $count = $this->getChangesetCount();
+
+ $expand_uri = $request_uri
+ ->alter('large', 'true')
+ ->setFragment('toc');
+
+ $message = array(
pht(
- 'This diff is large and affects %s files. '.
- 'You may load each file individually or ',
+ 'This large diff affects %s files. Files without inline '.
+ 'comments have been collapsed.',
new PhutilNumber($count)),
+ ' ',
phutil_tag(
- 'a',
- array(
- 'class' => 'button button-grey',
- 'href' => $request_uri
- ->alter('large', 'true')
- ->setFragment('toc'),
- ),
- pht('Show All Files Inline'))));
- $warning = $warning->render();
+ 'strong',
+ array(),
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $expand_uri,
+ ),
+ pht('Expand All Files'))),
+ );
+
+ $warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('Large Diff'))
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->appendChild($message);
+
+ $folded_changesets = $changesets;
+ } else {
+ $folded_changesets = array();
+ }
+ // Don't hide or fold changesets which have inline comments.
+ $hidden_changesets = $this->hiddenChangesets;
+ if ($hidden_changesets || $folded_changesets) {
$old = array_select_keys($changesets, $old_ids);
$new = array_select_keys($changesets, $new_ids);
$query = id(new DifferentialInlineCommentQuery())
->setViewer($viewer)
->needHidden(true)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
$inlines = $query->adjustInlinesForChangesets(
$inlines,
$old,
$new,
$revision);
- $visible_changesets = array();
foreach ($inlines as $inline) {
$changeset_id = $inline->getChangesetID();
- if (isset($changesets[$changeset_id])) {
- $visible_changesets[$changeset_id] = $changesets[$changeset_id];
+ if (!isset($changesets[$changeset_id])) {
+ continue;
}
+
+ unset($hidden_changesets[$changeset_id]);
+ unset($folded_changesets[$changeset_id]);
}
- } else {
- $warning = null;
- $visible_changesets = $changesets;
+ }
+
+ // If we would hide only one changeset, don't hide anything. The notice
+ // we'd render about it is about the same size as the changeset.
+ if (count($hidden_changesets) < 2) {
+ $hidden_changesets = array();
+ }
+
+ // Update the set of hidden changesets, since we may have just un-hidden
+ // some of them.
+ if ($hidden_changesets) {
+ $warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('Showing Only Differences'))
+ ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
+ ->appendChild(
+ pht(
+ 'This revision modifies %s more files that are hidden because '.
+ 'they were not modified between selected diffs and they have no '.
+ 'inline comments.',
+ phutil_count($hidden_changesets)));
+ }
+
+ // Compute the unfolded changesets. By default, everything is unfolded.
+ $unfolded_changesets = $changesets;
+ foreach ($folded_changesets as $changeset_id => $changeset) {
+ unset($unfolded_changesets[$changeset_id]);
+ }
+
+ // Throw away any hidden changesets.
+ foreach ($hidden_changesets as $changeset_id => $changeset) {
+ unset($changesets[$changeset_id]);
+ unset($unfolded_changesets[$changeset_id]);
}
$commit_hashes = mpull($diffs, 'getSourceControlBaseRevision');
$local_commits = idx($props, 'local:commits', array());
foreach ($local_commits as $local_commit) {
$commit_hashes[] = idx($local_commit, 'tree');
$commit_hashes[] = idx($local_commit, 'local');
}
$commit_hashes = array_unique(array_filter($commit_hashes));
if ($commit_hashes) {
$commits_for_links = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIdentifiers($commit_hashes)
->execute();
$commits_for_links = mpull(
$commits_for_links,
null,
'getCommitIdentifier');
} else {
$commits_for_links = array();
}
$header = $this->buildHeader($revision);
$subheader = $this->buildSubheaderView($revision);
$details = $this->buildDetails($revision, $field_list);
$curtain = $this->buildCurtain($revision);
$whitespace = $request->getStr(
'whitespace',
DifferentialChangesetParser::WHITESPACE_IGNORE_MOST);
$repository = $revision->getRepository();
if ($repository) {
$symbol_indexes = $this->buildSymbolIndexes(
$repository,
- $visible_changesets);
+ $unfolded_changesets);
} else {
$symbol_indexes = array();
}
$revision_warnings = $this->buildRevisionWarnings(
$revision,
$field_list,
$warning_handle_map,
$handles);
$info_view = null;
if ($revision_warnings) {
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($revision_warnings);
}
$detail_diffs = array_select_keys(
$diffs,
array($diff_vs, $target->getID()));
$detail_diffs = mpull($detail_diffs, null, 'getPHID');
$this->loadHarbormasterData($detail_diffs);
$diff_detail_box = $this->buildDiffDetailView(
$detail_diffs,
$revision,
$field_list);
$unit_box = $this->buildUnitMessagesView(
$target,
$revision);
$timeline = $this->buildTransactions(
$revision,
$diff_vs ? $diffs[$diff_vs] : $target,
$target,
$old_ids,
$new_ids);
$timeline->setQuoteRef($revision->getMonogram());
if ($this->isVeryLargeDiff()) {
$messages = array();
$messages[] = pht(
'This very large diff affects more than %s files. Use the %s to '.
'browse changes.',
new PhutilNumber($this->getVeryLargeDiffLimit()),
phutil_tag(
'a',
array(
'href' => '/differential/diff/'.$target->getID().'/changesets/',
),
phutil_tag('strong', array(), pht('Changeset List'))));
$changeset_view = id(new PHUIInfoView())
->setErrors($messages);
} else {
$changeset_view = id(new DifferentialChangesetListView())
->setChangesets($changesets)
- ->setVisibleChangesets($visible_changesets)
+ ->setVisibleChangesets($unfolded_changesets)
->setStandaloneURI('/differential/changeset/')
->setRawFileURIs(
'/differential/changeset/?view=old',
'/differential/changeset/?view=new')
->setUser($viewer)
->setDiff($target)
->setRenderingReferences($rendering_references)
->setVsMap($vs_map)
->setWhitespace($whitespace)
->setSymbolIndexes($symbol_indexes)
->setTitle(pht('Diff %s', $target->getID()))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$revision_id = $revision->getID();
$inline_list_uri = "/revision/inlines/{$revision_id}/";
$inline_list_uri = $this->getApplicationURI($inline_list_uri);
$changeset_view->setInlineListURI($inline_list_uri);
if ($repository) {
$changeset_view->setRepository($repository);
}
if (!$viewer_is_anonymous) {
$changeset_view->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision->getID().'/');
}
}
$broken_diffs = $this->loadHistoryDiffStatus($diffs);
$history = id(new DifferentialRevisionUpdateHistoryView())
->setUser($viewer)
->setDiffs($diffs)
->setDiffUnitStatuses($broken_diffs)
->setSelectedVersusDiffID($diff_vs)
->setSelectedDiffID($target->getID())
->setSelectedWhitespace($whitespace)
->setCommitsForLinks($commits_for_links);
$local_table = id(new DifferentialLocalCommitsView())
->setUser($viewer)
->setLocalCommits(idx($props, 'local:commits'))
->setCommitsForLinks($commits_for_links);
if ($repository && !$this->isVeryLargeDiff()) {
$other_revisions = $this->loadOtherRevisions(
$changesets,
$target,
$repository);
} else {
$other_revisions = array();
}
$other_view = null;
if ($other_revisions) {
$other_view = $this->renderOtherRevisions($other_revisions);
}
- $this->buildPackageMaps($changesets);
-
if ($this->isVeryLargeDiff()) {
$toc_view = null;
+
+ // When rendering a "very large" diff, we skip computation of owners
+ // that own no files because it is significantly expensive and not very
+ // valuable.
+ foreach ($revision->getReviewers() as $reviewer) {
+ // Give each reviewer a dummy nonempty value so the UI does not render
+ // the "(Owns No Changed Paths)" note. If that behavior becomes more
+ // sophisticated in the future, this behavior might also need to.
+ $reviewer->attachChangesets($changesets);
+ }
} else {
+ $this->buildPackageMaps($changesets);
+
$toc_view = $this->buildTableOfContents(
$changesets,
- $visible_changesets,
+ $unfolded_changesets,
$target->loadCoverageMap($viewer));
- }
- // Attach changesets to each reviewer so we can show which Owners package
- // reviewers own no files.
- foreach ($revision->getReviewers() as $reviewer) {
- $reviewer_phid = $reviewer->getReviewerPHID();
- $reviewer_changesets = $this->getPackageChangesets($reviewer_phid);
- $reviewer->attachChangesets($reviewer_changesets);
+ // Attach changesets to each reviewer so we can show which Owners package
+ // reviewers own no files.
+ foreach ($revision->getReviewers() as $reviewer) {
+ $reviewer_phid = $reviewer->getReviewerPHID();
+ $reviewer_changesets = $this->getPackageChangesets($reviewer_phid);
+ $reviewer->attachChangesets($reviewer_changesets);
+ }
}
- $tab_group = id(new PHUITabGroupView());
+ $tab_group = new PHUITabGroupView();
if ($toc_view) {
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Files'))
->setKey('files')
->appendChild($toc_view));
}
- $tab_group
- ->addTab(
- id(new PHUITabView())
- ->setName(pht('History'))
- ->setKey('history')
- ->appendChild($history))
- ->addTab(
- id(new PHUITabView())
- ->setName(pht('Commits'))
- ->setKey('commits')
- ->appendChild($local_table));
+ $tab_group->addTab(
+ id(new PHUITabView())
+ ->setName(pht('History'))
+ ->setKey('history')
+ ->appendChild($history));
+
+ $filetree_on = $viewer->compareUserSetting(
+ PhabricatorShowFiletreeSetting::SETTINGKEY,
+ PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE);
+
+ $collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY;
+ $filetree_collapsed = (bool)$viewer->getUserSetting($collapsed_key);
+
+ // See PHI811. If the viewer has the file tree on, the files tab with the
+ // table of contents is redundant, so default to the "History" tab instead.
+ if ($filetree_on && !$filetree_collapsed) {
+ $tab_group->selectTab('history');
+ }
+
+ $tab_group->addTab(
+ id(new PHUITabView())
+ ->setName(pht('Commits'))
+ ->setKey('commits')
+ ->appendChild($local_table));
$stack_graph = id(new DifferentialRevisionGraph())
->setViewer($viewer)
->setSeedPHID($revision->getPHID())
->setLoadEntireGraph(true)
->loadGraph();
if (!$stack_graph->isEmpty()) {
$stack_table = $stack_graph->newGraphTable();
$parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
$reachable = $stack_graph->getReachableObjects($parent_type);
foreach ($reachable as $key => $reachable_revision) {
if ($reachable_revision->isClosed()) {
unset($reachable[$key]);
}
}
if ($reachable) {
$stack_name = pht('Stack (%s Open)', phutil_count($reachable));
$stack_color = PHUIListItemView::STATUS_FAIL;
} else {
$stack_name = pht('Stack');
$stack_color = null;
}
$tab_group->addTab(
id(new PHUITabView())
->setName($stack_name)
->setKey('stack')
->setColor($stack_color)
->appendChild($stack_table));
}
if ($other_view) {
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Similar'))
->setKey('similar')
->appendChild($other_view));
}
$view_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Changeset List'))
->setHref('/differential/diff/'.$target->getID().'/changesets/')
->setIcon('fa-align-left');
$tab_header = id(new PHUIHeaderView())
->setHeader(pht('Revision Contents'))
->addActionLink($view_button);
$tab_view = id(new PHUIObjectBoxView())
->setHeader($tab_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
$signatures = DifferentialRequiredSignaturesField::loadForRevision(
$revision);
$missing_signatures = false;
foreach ($signatures as $phid => $signed) {
if (!$signed) {
$missing_signatures = true;
}
}
$footer = array();
$signature_message = null;
if ($missing_signatures) {
$signature_message = id(new PHUIInfoView())
->setTitle(pht('Content Hidden'))
->appendChild(
pht(
'The content of this revision is hidden until the author has '.
'signed all of the required legal agreements.'));
} else {
$anchor = id(new PhabricatorAnchorView())
->setAnchorName('toc')
->setNavigationMarker(true);
$footer[] = array(
$anchor,
- $warning,
+ $warnings,
$tab_view,
$changeset_view,
);
}
$comment_view = id(new DifferentialRevisionEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($revision);
$comment_view->setTransactionTimeline($timeline);
$review_warnings = array();
foreach ($field_list->getFields() as $field) {
$review_warnings[] = $field->getWarningsForDetailView();
}
$review_warnings = array_mergev($review_warnings);
if ($review_warnings) {
$warnings_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($review_warnings);
$comment_view->setInfoView($warnings_view);
}
$footer[] = $comment_view;
$monogram = $revision->getMonogram();
$operations_box = $this->buildOperationsBox($revision);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($monogram);
$crumbs->setBorder(true);
- $filetree_on = $viewer->compareUserSetting(
- PhabricatorShowFiletreeSetting::SETTINGKEY,
- PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE);
-
$nav = null;
if ($filetree_on && !$this->isVeryLargeDiff()) {
- $collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY;
- $collapsed_value = $viewer->getUserSetting($collapsed_key);
+ $width_key = PhabricatorFiletreeWidthSetting::SETTINGKEY;
+ $width_value = $viewer->getUserSetting($width_key);
$width_key = PhabricatorFiletreeWidthSetting::SETTINGKEY;
$width_value = $viewer->getUserSetting($width_key);
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle($monogram)
->setBaseURI(new PhutilURI($revision->getURI()))
- ->setCollapsed((bool)$collapsed_value)
+ ->setCollapsed($filetree_collapsed)
->setWidth((int)$width_value)
->build($changesets);
}
Javelin::initBehavior('differential-user-select');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setCurtain($curtain)
->setMainColumn(
array(
$operations_box,
$info_view,
$details,
$diff_detail_box,
$unit_box,
$timeline,
$signature_message,
))
->setFooter($footer);
$page = $this->newPage()
->setTitle($monogram.' '.$revision->getTitle())
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($revision->getPHID()))
->appendChild($view);
if ($nav) {
$page->setNavigation($nav);
}
return $page;
}
private function buildHeader(DifferentialRevision $revision) {
$view = id(new PHUIHeaderView())
->setHeader($revision->getTitle($revision))
->setUser($this->getViewer())
->setPolicyObject($revision)
->setHeaderIcon('fa-cog');
$status_tag = id(new PHUITagView())
->setName($revision->getStatusDisplayName())
->setIcon($revision->getStatusIcon())
->setColor($revision->getStatusTagColor())
->setType(PHUITagView::TYPE_SHADE);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_tag);
// If the revision is in a status other than "Draft", but not broadcasting,
// add an additional "Draft" tag to the header to make it clear that this
// revision hasn't promoted yet.
if (!$revision->getShouldBroadcast() && !$revision->isDraft()) {
$draft_status = DifferentialRevisionStatus::newForStatus(
DifferentialRevisionStatus::DRAFT);
$draft_tag = id(new PHUITagView())
->setName($draft_status->getDisplayName())
->setIcon($draft_status->getIcon())
->setColor($draft_status->getTagColor())
->setType(PHUITagView::TYPE_SHADE);
$view->addTag($draft_tag);
}
return $view;
}
private function buildSubheaderView(DifferentialRevision $revision) {
$viewer = $this->getViewer();
$author_phid = $revision->getAuthorPHID();
$author = $viewer->renderHandle($author_phid)->render();
$date = phabricator_datetime($revision->getDateCreated(), $viewer);
$author = phutil_tag('strong', array(), $author);
$handles = $viewer->loadHandles(array($author_phid));
$image_uri = $handles[$author_phid]->getImageURI();
$image_href = $handles[$author_phid]->getURI();
$content = pht('Authored by %s on %s.', $author, $date);
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
}
private function buildDetails(
DifferentialRevision $revision,
$custom_fields) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
if ($custom_fields) {
$custom_fields->appendFieldsToPropertyList(
$revision,
$viewer,
$properties);
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildCurtain(DifferentialRevision $revision) {
$viewer = $this->getViewer();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$curtain = $this->newCurtainView($revision);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$revision,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref("/differential/revision/edit/{$revision_id}/")
->setName(pht('Edit Revision'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-upload')
->setHref("/differential/revision/update/{$revision_id}/")
->setName(pht('Update Diff'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$request_uri = $this->getRequest()->getRequestURI();
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Download Raw Diff'))
->setHref($request_uri->alter('download', 'true')));
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$revision);
$revision_actions = array(
DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY,
DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY,
);
$revision_submenu = $relationship_list->newActionSubmenu($revision_actions)
->setName(pht('Edit Related Revisions...'))
->setIcon('fa-cog');
$curtain->addAction($revision_submenu);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
$repository = $revision->getRepository();
if ($repository && $repository->canPerformAutomation()) {
$revision_id = $revision->getID();
$op = new DrydockLandRepositoryOperation();
$barrier = $op->getBarrierToLanding($viewer, $revision);
if ($barrier) {
$can_land = false;
} else {
$can_land = true;
}
$action = id(new PhabricatorActionView())
->setName(pht('Land Revision'))
->setIcon('fa-fighter-jet')
->setHref("/differential/revision/operation/{$revision_id}/")
->setWorkflow(true)
->setDisabled(!$can_land);
$curtain->addAction($action);
}
return $curtain;
}
private function loadHistoryDiffStatus(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$diff_phids = mpull($diffs, 'getPHID');
$bad_unit_status = array(
ArcanistUnitTestResult::RESULT_FAIL,
ArcanistUnitTestResult::RESULT_BROKEN,
);
$message = new HarbormasterBuildUnitMessage();
$target = new HarbormasterBuildTarget();
$build = new HarbormasterBuild();
$buildable = new HarbormasterBuildable();
$broken_diffs = queryfx_all(
$message->establishConnection('r'),
'SELECT distinct a.buildablePHID
FROM %T m
JOIN %T t ON m.buildTargetPHID = t.phid
JOIN %T b ON t.buildPHID = b.phid
JOIN %T a ON b.buildablePHID = a.phid
WHERE a.buildablePHID IN (%Ls)
AND m.result in (%Ls)',
$message->getTableName(),
$target->getTableName(),
$build->getTableName(),
$buildable->getTableName(),
$diff_phids,
$bad_unit_status);
$unit_status = array();
foreach ($broken_diffs as $broken) {
$phid = $broken['buildablePHID'];
$unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL;
}
return $unit_status;
}
private function loadChangesetsAndVsMap(
DifferentialDiff $target,
DifferentialDiff $diff_vs = null,
PhabricatorRepository $repository = null) {
+ $viewer = $this->getViewer();
$load_diffs = array($target);
if ($diff_vs) {
$load_diffs[] = $diff_vs;
}
$raw_changesets = id(new DifferentialChangesetQuery())
- ->setViewer($this->getRequest()->getUser())
+ ->setViewer($viewer)
->withDiffs($load_diffs)
->execute();
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
- $refs = array();
- $vs_map = array();
+ $refs = array();
+ $vs_map = array();
$vs_changesets = array();
+ $must_compare = array();
if ($diff_vs) {
- $vs_id = $diff_vs->getID();
+ $vs_id = $diff_vs->getID();
$vs_changesets_path_map = array();
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
$vs_changesets_path_map[$path] = $changeset;
$vs_changesets[$changeset->getID()] = $changeset;
}
+
foreach ($changesets as $key => $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
if (isset($vs_changesets_path_map[$path])) {
$vs_map[$changeset->getID()] =
$vs_changesets_path_map[$path]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
unset($vs_changesets_path_map[$path]);
+
+ $must_compare[] = $changeset->getID();
+
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
+
foreach ($vs_changesets_path_map as $path => $changeset) {
$changesets[$changeset->getID()] = $changeset;
- $vs_map[$changeset->getID()] = -1;
- $refs[$changeset->getID()] = $changeset->getID().'/-1';
+ $vs_map[$changeset->getID()] = -1;
+ $refs[$changeset->getID()] = $changeset->getID().'/-1';
}
+
} else {
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
}
$changesets = msort($changesets, 'getSortKey');
+ // See T13137. When displaying the diff between two updates, hide any
+ // changesets which haven't actually changed.
+ $this->hiddenChangesets = array();
+ foreach ($must_compare as $changeset_id) {
+ $changeset = $changesets[$changeset_id];
+ $vs_changeset = $vs_changesets[$vs_map[$changeset_id]];
+
+ if ($changeset->hasSameEffectAs($vs_changeset)) {
+ $this->hiddenChangesets[$changeset_id] = $changesets[$changeset_id];
+ }
+ }
+
return array($changesets, $vs_map, $vs_changesets, $refs);
}
private function buildSymbolIndexes(
PhabricatorRepository $repository,
- array $visible_changesets) {
- assert_instances_of($visible_changesets, 'DifferentialChangeset');
+ array $unfolded_changesets) {
+ assert_instances_of($unfolded_changesets, 'DifferentialChangeset');
$engine = PhabricatorSyntaxHighlighter::newEngine();
$langs = $repository->getSymbolLanguages();
$langs = nonempty($langs, array());
$sources = $repository->getSymbolSources();
$sources = nonempty($sources, array());
$symbol_indexes = array();
if ($langs && $sources) {
$have_symbols = id(new DiffusionSymbolQuery())
->existsSymbolsInRepository($repository->getPHID());
if (!$have_symbols) {
return $symbol_indexes;
}
}
$repository_phids = array_merge(
array($repository->getPHID()),
$sources);
$indexed_langs = array_fill_keys($langs, true);
- foreach ($visible_changesets as $key => $changeset) {
+ foreach ($unfolded_changesets as $key => $changeset) {
$lang = $engine->getLanguageFromFilename($changeset->getFilename());
if (empty($indexed_langs) || isset($indexed_langs[$lang])) {
$symbol_indexes[$key] = array(
'lang' => $lang,
'repositories' => $repository_phids,
);
}
}
return $symbol_indexes;
}
private function loadOtherRevisions(
array $changesets,
DifferentialDiff $target,
PhabricatorRepository $repository) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $changeset->getAbsoluteRepositoryPath(
$repository,
$target);
}
if (!$paths) {
return array();
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (!$path_map) {
return array();
}
$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
$query = id(new DifferentialRevisionQuery())
->setViewer($this->getRequest()->getUser())
->withIsOpen(true)
->withUpdatedEpochBetween($recent, null)
->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
->setLimit(10)
->needFlags(true)
->needDrafts(true)
->needReviewers(true);
foreach ($path_map as $path => $path_id) {
$query->withPath($repository->getID(), $path_id);
}
$results = $query->execute();
// Strip out *this* revision.
foreach ($results as $key => $result) {
if ($result->getID() == $this->revisionID) {
unset($results[$key]);
}
}
return $results;
}
private function renderOtherRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$viewer = $this->getViewer();
$header = id(new PHUIHeaderView())
->setHeader(pht('Recent Similar Revisions'));
return id(new DifferentialRevisionListView())
->setViewer($viewer)
->setRevisions($revisions)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setNoBox(true);
}
/**
* Note this code is somewhat similar to the buildPatch method in
* @{class:DifferentialReviewRequestMail}.
*
* @return @{class:AphrontRedirectResponse}
*/
private function buildRawDiffResponse(
DifferentialRevision $revision,
array $changesets,
array $vs_changesets,
array $vs_map,
PhabricatorRepository $repository = null) {
assert_instances_of($changesets, 'DifferentialChangeset');
assert_instances_of($vs_changesets, 'DifferentialChangeset');
$viewer = $this->getViewer();
id(new DifferentialHunkQuery())
->setViewer($viewer)
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
$diff = new DifferentialDiff();
$diff->attachChangesets($changesets);
$raw_changes = $diff->buildChangesList();
$changes = array();
foreach ($raw_changes as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$loader = id(new PhabricatorFileBundleLoader())
->setViewer($viewer);
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
$vcs = $repository ? $repository->getVersionControlSystem() : null;
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$raw_diff = $bundle->toGitPatch();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
default:
$raw_diff = $bundle->toUnifiedDiff();
break;
}
$request_uri = $this->getRequest()->getRequestURI();
// this ends up being something like
// D123.diff
// or the verbose
// D123.vs123.id123.whitespaceignore-all.diff
// 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';
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$raw_diff,
array(
'name' => $file_name,
'ttl.relative' => phutil_units('24 hours in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file->attachToObject($revision->getPHID());
unset($unguarded);
return $file->getRedirectResponse();
}
private function buildTransactions(
DifferentialRevision $revision,
DifferentialDiff $left_diff,
DifferentialDiff $right_diff,
array $old_ids,
array $new_ids) {
$timeline = $this->buildTransactionTimeline(
$revision,
new DifferentialTransactionQuery(),
$engine = null,
array(
'left' => $left_diff->getID(),
'right' => $right_diff->getID(),
'old' => implode(',', $old_ids),
'new' => implode(',', $new_ids),
));
return $timeline;
}
private function buildRevisionWarnings(
DifferentialRevision $revision,
PhabricatorCustomFieldList $field_list,
array $warning_handle_map,
array $handles) {
$warnings = array();
foreach ($field_list->getFields() as $key => $field) {
$phids = idx($warning_handle_map, $key, array());
$field_handles = array_select_keys($handles, $phids);
$field_warnings = $field->getWarningsForRevisionHeader($field_handles);
foreach ($field_warnings as $warning) {
$warnings[] = $warning;
}
}
return $warnings;
}
private function buildDiffDetailView(
array $diffs,
DifferentialRevision $revision,
PhabricatorCustomFieldList $field_list) {
$viewer = $this->getViewer();
$fields = array();
foreach ($field_list->getFields() as $field) {
if ($field->shouldAppearInDiffPropertyView()) {
$fields[] = $field;
}
}
if (!$fields) {
return null;
}
$property_lists = array();
foreach ($this->getDiffTabLabels($diffs) as $tab) {
list($label, $diff) = $tab;
$property_lists[] = array(
$label,
$this->buildDiffPropertyList($diff, $revision, $fields),
);
}
$tab_group = id(new PHUITabGroupView())
->setHideSingleTab(true);
foreach ($property_lists as $key => $property_list) {
list($tab_name, $list_view) = $property_list;
$tab = id(new PHUITabView())
->setKey($key)
->setName($tab_name)
->appendChild($list_view);
$tab_group->addTab($tab);
$tab_group->selectTab($key);
}
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Diff Detail'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setUser($viewer)
->addTabGroup($tab_group);
}
private function buildDiffPropertyList(
DifferentialDiff $diff,
DifferentialRevision $revision,
array $fields) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($diff);
foreach ($fields as $field) {
$label = $field->renderDiffPropertyViewLabel($diff);
$value = $field->renderDiffPropertyViewValue($diff);
if ($value !== null) {
$view->addProperty($label, $value);
}
}
return $view;
}
private function buildOperationsBox(DifferentialRevision $revision) {
$viewer = $this->getViewer();
// Save a query if we can't possibly have pending operations.
$repository = $revision->getRepository();
if (!$repository || !$repository->canPerformAutomation()) {
return null;
}
$operations = id(new DrydockRepositoryOperationQuery())
->setViewer($viewer)
->withObjectPHIDs(array($revision->getPHID()))
->withIsDismissed(false)
->withOperationTypes(
array(
DrydockLandRepositoryOperation::OPCONST,
))
->execute();
if (!$operations) {
return null;
}
$state_fail = DrydockRepositoryOperation::STATE_FAIL;
// We're going to show the oldest operation which hasn't failed, or the
// most recent failure if they're all failures.
$operations = msort($operations, 'getID');
foreach ($operations as $operation) {
if ($operation->getOperationState() != $state_fail) {
break;
}
}
// If we found a completed operation, don't render anything. We don't want
// to show an older error after the thing worked properly.
if ($operation->isDone()) {
return null;
}
$box_view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Active Operations'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
return id(new DrydockRepositoryOperationStatusView())
->setUser($viewer)
->setBoxView($box_view)
->setOperation($operation);
}
private function buildUnitMessagesView(
$diff,
DifferentialRevision $revision) {
$viewer = $this->getViewer();
if (!$diff->getBuildable()) {
return null;
}
if (!$diff->getUnitMessages()) {
return null;
}
$interesting_messages = array();
foreach ($diff->getUnitMessages() as $message) {
switch ($message->getResult()) {
case ArcanistUnitTestResult::RESULT_PASS:
case ArcanistUnitTestResult::RESULT_SKIP:
break;
default:
$interesting_messages[] = $message;
break;
}
}
if (!$interesting_messages) {
return null;
}
$excuse = null;
if ($diff->hasDiffProperty('arc:unit-excuse')) {
$excuse = $diff->getProperty('arc:unit-excuse');
}
return id(new HarbormasterUnitSummaryView())
- ->setUser($viewer)
+ ->setViewer($viewer)
->setExcuse($excuse)
->setBuildable($diff->getBuildable())
->setUnitMessages($diff->getUnitMessages())
->setLimit(5)
->setShowViewAll(true);
}
+ private function getOldDiffID(DifferentialRevision $revision, array $diffs) {
+ assert_instances_of($diffs, 'DifferentialDiff');
+ $request = $this->getRequest();
+
+ $diffs = mpull($diffs, null, 'getID');
+
+ $is_new = ($request->getURIData('filter') === 'new');
+ $old_id = $request->getInt('vs');
+
+ // This is ambiguous, so just 404 rather than trying to figure out what
+ // the user expects.
+ if ($is_new && $old_id) {
+ return new Aphront404Response();
+ }
+
+ if ($is_new) {
+ $viewer = $this->getViewer();
+
+ $xactions = id(new DifferentialTransactionQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($revision->getPHID()))
+ ->withAuthorPHIDs(array($viewer->getPHID()))
+ ->setOrder('newest')
+ ->setLimit(1)
+ ->execute();
+
+ if (!$xactions) {
+ $this->warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('No Actions'))
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->appendChild(
+ pht(
+ 'Showing all changes because you have never taken an '.
+ 'action on this revision.'));
+ } else {
+ $xaction = head($xactions);
+
+ // Find the transactions which updated this revision. We want to
+ // figure out which diff was active when you last took an action.
+ $updates = id(new DifferentialTransactionQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($revision->getPHID()))
+ ->withTransactionTypes(
+ array(
+ DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE,
+ ))
+ ->setOrder('oldest')
+ ->execute();
+
+ // Sort the diffs into two buckets: those older than your last action
+ // and those newer than your last action.
+ $older = array();
+ $newer = array();
+ foreach ($updates as $update) {
+ // If you updated the revision with "arc diff", try to count that
+ // update as "before your last action".
+ if ($update->getDateCreated() <= $xaction->getDateCreated()) {
+ $older[] = $update->getNewValue();
+ } else {
+ $newer[] = $update->getNewValue();
+ }
+ }
+
+ if (!$newer) {
+ $this->warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('No Recent Updates'))
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->appendChild(
+ pht(
+ 'Showing all changes because the diff for this revision '.
+ 'has not been updated since your last action.'));
+ } else {
+ $older = array_fuse($older);
+
+ // Find the most recent diff from before the last action.
+ $old = null;
+ foreach ($diffs as $diff) {
+ if (!isset($older[$diff->getPHID()])) {
+ break;
+ }
+
+ $old = $diff;
+ }
+
+ // It's possible we may not find such a diff: transactions may have
+ // been removed from the database, for example. If we miss, just
+ // fail into some reasonable state since 404'ing would be perplexing.
+ if ($old) {
+ $this->warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('New Changes Shown'))
+ ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
+ ->appendChild(
+ pht(
+ 'Showing changes since the last action you took on this '.
+ 'revision.'));
+
+ $old_id = $old->getID();
+ }
+ }
+ }
+ }
+
+ if (isset($diffs[$old_id])) {
+ return $old_id;
+ }
+
+ return null;
+ }
+
+ private function getNewDiffID(DifferentialRevision $revision, array $diffs) {
+ assert_instances_of($diffs, 'DifferentialDiff');
+ $request = $this->getRequest();
+
+ $diffs = mpull($diffs, null, 'getID');
+
+ $is_new = ($request->getURIData('filter') === 'new');
+ $new_id = $request->getInt('id');
+
+ if ($is_new && $new_id) {
+ return new Aphront404Response();
+ }
+
+ if (isset($diffs[$new_id])) {
+ return $new_id;
+ }
+
+ return (int)last_key($diffs);
+ }
+
}
diff --git a/src/applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php b/src/applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php
index a6940b536..1f1adbc83 100644
--- a/src/applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php
+++ b/src/applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php
@@ -1,117 +1,117 @@
<?php
final class DifferentialRevisionDependedOnByRevisionEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 6;
public function getInverseEdgeConstant() {
return DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
public function getConduitKey() {
return 'revision.child';
}
public function getConduitName() {
return pht('Revision Has Child');
}
public function getConduitDescription() {
return pht(
'The source revision makes changes required by the destination '.
'revision.');
}
public function getTransactionAddString(
$actor,
$add_count,
$add_edges) {
return pht(
- '%s added %s dependent revision(s): %s.',
+ '%s added %s child revision(s): %s.',
$actor,
$add_count,
$add_edges);
}
public function getTransactionRemoveString(
$actor,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s dependent revision(s): %s.',
+ '%s removed %s child revision(s): %s.',
$actor,
$rem_count,
$rem_edges);
}
public function getTransactionEditString(
$actor,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
- '%s edited dependent revision(s), added %s: %s; removed %s: %s.',
+ '%s edited child revision(s), added %s: %s; removed %s: %s.',
$actor,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
public function getFeedAddString(
$actor,
$object,
$add_count,
$add_edges) {
return pht(
- '%s added %s dependent revision(s) for %s: %s.',
+ '%s added %s child revision(s) for %s: %s.',
$actor,
$add_count,
$object,
$add_edges);
}
public function getFeedRemoveString(
$actor,
$object,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s dependent revision(s) for %s: %s.',
+ '%s removed %s child revision(s) for %s: %s.',
$actor,
$rem_count,
$object,
$rem_edges);
}
public function getFeedEditString(
$actor,
$object,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
- '%s edited dependent revision(s) for %s, added %s: %s; removed %s: %s.',
+ '%s edited child revision(s) for %s, added %s: %s; removed %s: %s.',
$actor,
$object,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
}
diff --git a/src/applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php b/src/applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php
index e826f554c..a972b5359 100644
--- a/src/applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php
+++ b/src/applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php
@@ -1,120 +1,120 @@
<?php
final class DifferentialRevisionDependsOnRevisionEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 5;
public function getInverseEdgeConstant() {
return DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
public function shouldPreventCycles() {
return true;
}
public function getConduitKey() {
return 'revision.parent';
}
public function getConduitName() {
return pht('Revision Has Parent');
}
public function getConduitDescription() {
return pht(
'The source revision depends on changes in the destination revision.');
}
public function getTransactionAddString(
$actor,
$add_count,
$add_edges) {
return pht(
- '%s added %s dependencie(s): %s.',
+ '%s added %s parent revision(s): %s.',
$actor,
$add_count,
$add_edges);
}
public function getTransactionRemoveString(
$actor,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s dependencie(s): %s.',
+ '%s removed %s parent revision(s): %s.',
$actor,
$rem_count,
$rem_edges);
}
public function getTransactionEditString(
$actor,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
- '%s edited dependencie(s), added %s: %s; removed %s: %s.',
+ '%s edited parent revision(s), added %s: %s; removed %s: %s.',
$actor,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
public function getFeedAddString(
$actor,
$object,
$add_count,
$add_edges) {
return pht(
- '%s added %s dependencie(s) for %s: %s.',
+ '%s added %s parent revision(s) for %s: %s.',
$actor,
$add_count,
$object,
$add_edges);
}
public function getFeedRemoveString(
$actor,
$object,
$rem_count,
$rem_edges) {
return pht(
- '%s removed %s dependencie(s) for %s: %s.',
+ '%s removed %s parent revision(s) for %s: %s.',
$actor,
$rem_count,
$object,
$rem_edges);
}
public function getFeedEditString(
$actor,
$object,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
- '%s edited dependencie(s) for %s, added %s: %s; removed %s: %s.',
+ '%s edited parent revision(s) for %s, added %s: %s; removed %s: %s.',
$actor,
$object,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
}
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index 50094b02f..c452cb934 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1668 +1,1762 @@
<?php
final class DifferentialTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $changedPriorToCommitURI;
private $isCloseByCommit;
private $repositoryPHIDOverride = false;
private $didExpandInlineState = false;
- private $affectedPaths;
private $firstBroadcast = false;
private $wasBroadcasting;
private $isDraftDemotion;
+ private $ownersDiff;
+ private $ownersChangesets;
+
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getEditorObjectsDescription() {
return pht('Differential Revisions');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this revision.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function isFirstBroadcast() {
return $this->firstBroadcast;
}
public function getDiffUpdateTransaction(array $xactions) {
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_update) {
return $xaction;
}
}
return null;
}
public function setIsCloseByCommit($is_close_by_commit) {
$this->isCloseByCommit = $is_close_by_commit;
return $this;
}
public function getIsCloseByCommit() {
return $this->isCloseByCommit;
}
public function setChangedPriorToCommitURI($uri) {
$this->changedPriorToCommitURI = $uri;
return $this;
}
public function getChangedPriorToCommitURI() {
return $this->changedPriorToCommitURI;
}
public function setRepositoryPHIDOverride($phid_or_null) {
$this->repositoryPHIDOverride = $phid_or_null;
return $this;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
$types[] = DifferentialTransaction::TYPE_INLINE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
// If we have an "Inline State" transaction already, the caller
// built it for us so we don't need to expand it again.
$this->didExpandInlineState = true;
break;
case DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE:
if ($xaction->getMetadataValue('draft.demote')) {
$this->isDraftDemotion = true;
}
break;
}
}
$this->wasBroadcasting = $object->getShouldBroadcast();
return parent::expandTransactions($object, $xactions);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$actor = $this->getActor();
$actor_phid = $this->getActingAsPHID();
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$want_downgrade = array();
$must_downgrade = array();
if ($this->getIsCloseByCommit()) {
// Never downgrade reviewers when we're closing a revision after a
// commit.
} else {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
if (!$object->isChangePlanned()) {
// If the old state isn't "Changes Planned", downgrade the accepts
// even if they're sticky.
// We don't downgrade for "Changes Planned" to allow an author to
// undo a "Plan Changes" by immediately following it up with a
// "Request Review".
$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
$must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
}
$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
}
}
if ($want_downgrade) {
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
$results[] = id(new DifferentialTransaction())
->setTransactionType($void_type)
->setIgnoreOnNoEffect(true)
->setMetadataValue('void.force', $must_downgrade)
->setNewValue($want_downgrade);
}
$is_commandeer = false;
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
if ($this->getIsCloseByCommit()) {
// Don't bother with any of this if this update is a side effect of
// commit detection.
break;
}
// When a revision is updated and the diff comes from a branch named
// "T123" or similar, automatically associate the commit with the
// task that the branch names.
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$diff = $this->requireDiff($xaction->getNewValue());
$branch = $diff->getBranch();
// No "$", to allow for branches like T123_demo.
$match = null;
if (preg_match('/^T(\d+)/i', $branch, $match)) {
$task_id = $match[1];
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array($task_id))
->execute();
if ($tasks) {
$task = head($tasks);
$task_phid = $task->getPHID();
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_ref_task)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => array($task_phid => $task_phid)));
}
}
}
break;
case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE:
$is_commandeer = true;
break;
}
if ($is_commandeer) {
$results[] = $this->newCommandeerReviewerTransaction($object);
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
case DifferentialTransaction::TYPE_INLINE:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
- $actor_is_author = ($object->getAuthorPHID() == $actor_phid);
- if (!$actor_is_author) {
- break;
- }
+ $author_phid = $object->getAuthorPHID();
+ $actor_is_author = ($actor_phid == $author_phid);
$state_map = PhabricatorTransactions::getInlineStateMap();
- $inlines = id(new DifferentialDiffInlineCommentQuery())
+ $query = id(new DifferentialDiffInlineCommentQuery())
->setViewer($this->getActor())
->withRevisionPHIDs(array($object->getPHID()))
- ->withFixedStates(array_keys($state_map))
+ ->withFixedStates(array_keys($state_map));
+
+ $inlines = array();
+
+ // We're going to undraft any "done" marks on your own inlines.
+ $inlines[] = id(clone $query)
+ ->withAuthorPHIDs(array($actor_phid))
+ ->withHasTransaction(false)
->execute();
+ // If you're the author, we also undraft any "done" marks on other
+ // inlines.
+ if ($actor_is_author) {
+ $inlines[] = id(clone $query)
+ ->withHasTransaction(true)
+ ->execute();
+ }
+
+ $inlines = array_mergev($inlines);
+
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$results[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $results;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new DifferentialTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load the most up-to-date version of the revision and its reviewers,
// so we don't need to try to deduce the state of reviewers by examining
// all the changes made by the transactions. Then, update the reviewers
// on the object to make sure we're acting on the current reviewer set
// (and, for example, sending mail to the right people).
$new_revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
if (!$new_revision) {
throw new Exception(
pht('Failed to load revision from transaction finalization.'));
}
$object->attachReviewers($new_revision->getReviewers());
$object->attachActiveDiff($new_revision->getActiveDiff());
$object->attachRepository($new_revision->getRepository());
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$diff = $this->requireDiff($xaction->getNewValue(), true);
// Update these denormalized index tables when we attach a new
// diff to a revision.
$this->updateRevisionHashTable($object, $diff);
$this->updateAffectedPathTable($object, $diff);
break;
}
}
$xactions = $this->updateReviewStatus($object, $xactions);
$this->markReviewerComments($object, $xactions);
return $xactions;
}
private function updateReviewStatus(
DifferentialRevision $revision,
array $xactions) {
$was_accepted = $revision->isAccepted();
$was_revision = $revision->isNeedsRevision();
$was_review = $revision->isNeedsReview();
if (!$was_accepted && !$was_revision && !$was_review) {
// Revisions can't transition out of other statuses (like closed or
// abandoned) as a side effect of reviewer status changes.
return $xactions;
}
// Try to move a revision to "accepted". We look for:
//
// - at least one accepting reviewer who is a user; and
// - no rejects; and
// - no rejects of older diffs; and
// - no blocking reviewers.
$has_accepting_user = false;
$has_rejecting_reviewer = false;
$has_rejecting_older_reviewer = false;
$has_blocking_reviewer = false;
$active_diff = $revision->getActiveDiff();
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_status = $reviewer->getReviewerStatus();
switch ($reviewer_status) {
case DifferentialReviewerStatus::STATUS_REJECTED:
$active_phid = $active_diff->getPHID();
if ($reviewer->isRejected($active_phid)) {
$has_rejecting_reviewer = true;
} else {
$has_rejecting_older_reviewer = true;
}
break;
case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
$has_rejecting_older_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_BLOCKING:
$has_blocking_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_ACCEPTED:
if ($reviewer->isUser()) {
$active_phid = $active_diff->getPHID();
if ($reviewer->isAccepted($active_phid)) {
$has_accepting_user = true;
}
}
break;
}
}
$new_status = null;
if ($has_accepting_user &&
!$has_rejecting_reviewer &&
!$has_rejecting_older_reviewer &&
!$has_blocking_reviewer) {
$new_status = DifferentialRevisionStatus::ACCEPTED;
} else if ($has_rejecting_reviewer) {
// This isn't accepted, and there's at least one rejecting reviewer,
// so the revision needs changes. This usually happens after a
// "reject".
$new_status = DifferentialRevisionStatus::NEEDS_REVISION;
} else if ($was_accepted) {
// This revision was accepted, but it no longer satisfies the
// conditions for acceptance. This usually happens after an accepting
// reviewer resigns or is removed.
$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
}
if ($new_status === null) {
return $xactions;
}
$old_status = $revision->getModernRevisionStatus();
if ($new_status == $old_status) {
return $xactions;
}
$xaction = id(new DifferentialTransaction())
->setTransactionType(
DifferentialRevisionStatusTransaction::TRANSACTIONTYPE)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($revision, $xaction)
->save();
$xactions[] = $xaction;
// Save the status adjustment we made earlier.
$revision
->setModernRevisionStatus($new_status)
->save();
return $xactions;
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == DifferentialTransaction::TYPE_INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$object->getShouldBroadcast()) {
return false;
}
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
if ($object->getShouldBroadcast()) {
$this->requireReviewers($object);
$phids = array();
$phids[] = $object->getAuthorPHID();
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
continue;
}
$phids[] = $reviewer->getReviewerPHID();
}
return $phids;
}
// If we're demoting a draft after a build failure, just notify the author.
if ($this->isDraftDemotion) {
$author_phid = $object->getAuthorPHID();
return array(
$author_phid,
);
}
return array();
}
protected function getMailCC(PhabricatorLiskDAO $object) {
if (!$object->getShouldBroadcast()) {
return array();
}
return parent::getMailCC($object);
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
$this->requireReviewers($object);
$phids = array();
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
$phids[] = $reviewer->getReviewerPHID();
}
}
return $phids;
}
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
$show_lines = false;
if ($this->isFirstBroadcast()) {
$action = pht('Request');
$show_lines = true;
} else {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
if ($strongest->getTransactionType() == $type_update) {
$show_lines = true;
}
}
if ($show_lines) {
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s] [%s', $action, $object->getRevisionScaleGlyphs());
}
return $action;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// This is nonstandard, but retains threading with older messages.
$phid = $object->getPHID();
return "differential-rev-{$phid}-req";
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new DifferentialReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- $id = $object->getID();
+ $monogram = $object->getMonogram();
$title = $object->getTitle();
- $subject = "D{$id}: {$title}";
return id(new PhabricatorMetaMTAMail())
- ->setSubject($subject);
+ ->setSubject(pht('%s: %s', $monogram, $title))
+ ->setMustEncryptSubject(pht('%s: Revision Updated', $monogram))
+ ->setMustEncryptURI($object->getURI());
}
protected function getTransactionsForMail(
PhabricatorLiskDAO $object,
array $xactions) {
// If this is the first time we're sending mail about this revision, we
// generate mail for all prior transactions, not just whatever is being
// applied now. This gets the "added reviewers" lines and other relevant
// information into the mail.
if ($this->isFirstBroadcast()) {
return $this->loadUnbroadcastTransactions($object);
}
return $xactions;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$viewer = $this->requireActor();
$body = new PhabricatorMetaMTAMailBody();
$body->setViewer($this->requireActor());
- $revision_uri = PhabricatorEnv::getProductionURI('/D'.$object->getID());
+ $revision_uri = $object->getURI();
+ $revision_uri = PhabricatorEnv::getProductionURI($revision_uri);
+ $new_uri = $revision_uri.'/new/';
$this->addHeadersAndCommentsToMailBody(
$body,
$xactions,
pht('View Revision'),
$revision_uri);
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
}
if ($inlines) {
$this->appendInlineCommentsForMail($object, $inlines, $body);
}
+ $update_xaction = null;
+ foreach ($xactions as $xaction) {
+ switch ($xaction->getTransactionType()) {
+ case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
+ $update_xaction = $xaction;
+ break;
+ }
+ }
+
+ if ($update_xaction) {
+ $diff = $this->requireDiff($update_xaction->getNewValue(), true);
+ } else {
+ $diff = null;
+ }
+
$changed_uri = $this->getChangedPriorToCommitURI();
if ($changed_uri) {
$body->addLinkSection(
pht('CHANGED PRIOR TO COMMIT'),
$changed_uri);
}
$this->addCustomFieldsToMailBody($body, $object, $xactions);
+ if (!$this->isFirstBroadcast()) {
+ $body->addLinkSection(pht('CHANGES SINCE LAST ACTION'), $new_uri);
+ }
+
$body->addLinkSection(
pht('REVISION DETAIL'),
$revision_uri);
- $update_xaction = null;
- foreach ($xactions as $xaction) {
- switch ($xaction->getTransactionType()) {
- case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
- $update_xaction = $xaction;
- break;
- }
- }
-
if ($update_xaction) {
- $diff = $this->requireDiff($update_xaction->getNewValue(), true);
-
$body->addTextSection(
pht('AFFECTED FILES'),
$this->renderAffectedFilesForMail($diff));
$config_key_inline = 'metamta.differential.inline-patches';
$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
$config_key_attach = 'metamta.differential.attach-patches';
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
if ($config_inline || $config_attach) {
$body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
- $patch = $this->buildPatchForMail($diff);
- if ($config_inline) {
+ try {
+ $patch = $this->buildPatchForMail($diff, $body_limit);
+ } catch (ArcanistDiffByteSizeException $ex) {
+ $patch = null;
+ }
+
+ if (($patch !== null) && $config_inline) {
$lines = substr_count($patch, "\n");
$bytes = strlen($patch);
// Limit the patch size to the smaller of 256 bytes per line or
// the mail body limit. This prevents degenerate behavior for patches
// with one line that is 10MB long. See T11748.
$byte_limits = array();
$byte_limits[] = (256 * $config_inline);
$byte_limits[] = $body_limit;
$byte_limit = min($byte_limits);
$lines_ok = ($lines <= $config_inline);
$bytes_ok = ($bytes <= $byte_limit);
if ($lines_ok && $bytes_ok) {
$this->appendChangeDetailsForMail($object, $diff, $patch, $body);
} else {
// TODO: Provide a helpful message about the patch being too
// large or lengthy here.
}
}
- if ($config_attach) {
+ if (($patch !== null) && $config_attach) {
// See T12033, T11767, and PHI55. This is a crude fix to stop the
// major concrete problems that lackluster email size limits cause.
if (strlen($patch) < $body_limit) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
new PhabricatorMetaMTAAttachment($patch, $name, $mime_type));
}
}
}
}
return $body;
}
public function getMailTagsMap() {
return array(
DifferentialTransaction::MAILTAG_REVIEW_REQUEST =>
pht('A revision is created.'),
DifferentialTransaction::MAILTAG_UPDATED =>
pht('A revision is updated.'),
DifferentialTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a revision.'),
DifferentialTransaction::MAILTAG_CLOSED =>
pht('A revision is closed.'),
DifferentialTransaction::MAILTAG_REVIEWERS =>
pht("A revision's reviewers change."),
DifferentialTransaction::MAILTAG_CC =>
pht("A revision's CCs change."),
DifferentialTransaction::MAILTAG_OTHER =>
pht('Other revision activity not listed above occurs.'),
);
}
protected function supportsSearch() {
return true;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
// For "Fixes ..." and "Depends on ...", we're only going to look at
// content blocks which are part of the revision itself (like "Summary"
// and "Test Plan"), not comments.
$content_parts = array();
foreach ($changes as $change) {
if ($change->getTransaction()->isCommentTransaction()) {
continue;
}
$content_parts[] = $change->getNewValue();
}
if (!$content_parts) {
return array();
}
$content_block = implode("\n\n", $content_parts);
$task_map = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($content_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$task_id = (int)trim($monogram, 'tT');
$task_map[$task_id] = true;
}
}
$rev_map = array();
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($content_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$rev_id = (int)trim($monogram, 'dD');
$rev_map[$rev_id] = true;
}
}
$edges = array();
$task_phids = array();
$rev_phids = array();
if ($task_map) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array_keys($task_map))
->execute();
if ($tasks) {
$task_phids = mpull($tasks, 'getPHID', 'getPHID');
$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$edges[$edge_related] = $task_phids;
}
}
if ($rev_map) {
$revs = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withIDs(array_keys($rev_map))
->execute();
$rev_phids = mpull($revs, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a revision
// depends upon itself.
unset($rev_phids[$object->getPHID()]);
if ($revs) {
$depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
$edges[$depends] = $rev_phids;
}
}
$revert_refs = id(new DifferentialCustomFieldRevertsParser())
->parseCorpus($content_block);
$revert_monograms = array();
foreach ($revert_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$revert_monograms[] = $monogram;
}
}
if ($revert_monograms) {
$revert_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withNames($revert_monograms)
->withTypes(
array(
DifferentialRevisionPHIDType::TYPECONST,
PhabricatorRepositoryCommitPHIDType::TYPECONST,
))
->execute();
$revert_phids = mpull($revert_objects, 'getPHID', 'getPHID');
// Don't let an object revert itself, although other silly stuff like
// cycles of objects reverting each other is not prevented.
unset($revert_phids[$object->getPHID()]);
$revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
$edges[$revert_type] = $revert_phids;
} else {
$revert_phids = array();
}
// See PHI574. Respect any unmentionable PHIDs which were set on the
// Editor by the caller.
$unmentionable_map = $this->getUnmentionablePHIDMap();
$unmentionable_map += $task_phids;
$unmentionable_map += $rev_phids;
$unmentionable_map += $revert_phids;
$this->setUnmentionablePHIDMap($unmentionable_map);
$result = array();
foreach ($edges as $type => $specs) {
$result[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type)
->setNewValue(array('+' => $specs));
}
return $result;
}
private function appendInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inlines,
PhabricatorMetaMTAMailBody $body) {
+ $limit = 100;
+ $limit_note = null;
+ if (count($inlines) > $limit) {
+ $limit_note = pht(
+ '(Showing first %s of %s inline comments.)',
+ new PhutilNumber($limit),
+ phutil_count($inlines));
+
+ $inlines = array_slice($inlines, 0, $limit, true);
+ }
+
$section = id(new DifferentialInlineCommentMailView())
->setViewer($this->getActor())
->setInlines($inlines)
->buildMailSection();
$header = pht('INLINE COMMENTS');
$section_text = "\n".$section->getPlaintext();
+ if ($limit_note) {
+ $section_text = $limit_note."\n".$section_text;
+ }
$style = array(
'margin: 6px 0 12px 0;',
);
$section_html = phutil_tag(
'div',
array(
'style' => implode(' ', $style),
),
$section->getHTML());
+ if ($limit_note) {
+ $section_html = array(
+ phutil_tag(
+ 'em',
+ array(),
+ $limit_note),
+ $section_html,
+ );
+ }
+
$body->addPlaintextSection($header, $section_text, false);
$body->addHTMLSection($header, $section_html);
}
private function appendChangeDetailsForMail(
PhabricatorLiskDAO $object,
DifferentialDiff $diff,
$patch,
PhabricatorMetaMTAMailBody $body) {
$section = id(new DifferentialChangeDetailMailView())
->setViewer($this->getActor())
->setDiff($diff)
->setPatch($patch)
->buildMailSection();
$header = pht('CHANGE DETAILS');
$section_text = "\n".$section->getPlaintext();
$style = array(
'margin: 6px 0 12px 0;',
);
$section_html = phutil_tag(
'div',
array(
'style' => implode(' ', $style),
),
$section->getHTML());
$body->addPlaintextSection($header, $section_text, false);
$body->addHTMLSection($header, $section_html);
}
private function loadDiff($phid, $need_changesets = false) {
$query = id(new DifferentialDiffQuery())
->withPHIDs(array($phid))
->setViewer($this->getActor());
if ($need_changesets) {
$query->needChangesets(true);
}
return $query->executeOne();
}
public function requireDiff($phid, $need_changesets = false) {
$diff = $this->loadDiff($phid, $need_changesets);
if (!$diff) {
throw new Exception(pht('Diff "%s" does not exist!', $phid));
}
return $diff;
}
/* -( Herald Integration )------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$repository = $object->getRepository();
if (!$repository) {
return array();
}
- if (!$this->affectedPaths) {
+ $diff = $this->ownersDiff;
+ $changesets = $this->ownersChangesets;
+
+ $this->ownersDiff = null;
+ $this->ownersChangesets = null;
+
+ if (!$changesets) {
return array();
}
- $packages = PhabricatorOwnersPackage::loadAffectedPackages(
+ $packages = PhabricatorOwnersPackage::loadAffectedPackagesForChangesets(
$repository,
- $this->affectedPaths);
+ $diff,
+ $changesets);
if (!$packages) {
return array();
}
// Identify the packages with "Non-Owner Author" review rules and remove
// them if the author has authority over the package.
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$need_authority = array();
foreach ($packages as $package) {
$autoreview_setting = $package->getAutoReview();
$spec = idx($autoreview_map, $autoreview_setting);
if (!$spec) {
continue;
}
if (idx($spec, 'authority')) {
$need_authority[$package->getPHID()] = $package->getPHID();
}
}
if ($need_authority) {
$authority = id(new PhabricatorOwnersPackageQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($need_authority)
->withAuthorityPHIDs(array($object->getAuthorPHID()))
->execute();
$authority = mpull($authority, null, 'getPHID');
foreach ($packages as $key => $package) {
$package_phid = $package->getPHID();
if (isset($authority[$package_phid])) {
unset($packages[$key]);
continue;
}
}
if (!$packages) {
return array();
}
}
$auto_subscribe = array();
$auto_review = array();
$auto_block = array();
foreach ($packages as $package) {
switch ($package->getAutoReview()) {
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS:
$auto_review[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS:
$auto_block[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE:
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS:
$auto_subscribe[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_NONE:
default:
break;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$xactions = array();
if ($auto_subscribe) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => mpull($auto_subscribe, 'getPHID'),
));
}
$specs = array(
array($auto_review, false),
array($auto_block, true),
);
foreach ($specs as $spec) {
list($reviewers, $blocking) = $spec;
if (!$reviewers) {
continue;
}
$phids = mpull($reviewers, 'getPHID');
$xaction = $this->newAutoReviewTransaction($object, $phids, $blocking);
if ($xaction) {
$xactions[] = $xaction;
}
}
return $xactions;
}
private function newAutoReviewTransaction(
PhabricatorLiskDAO $object,
array $phids,
$is_blocking) {
// TODO: This is substantially similar to DifferentialReviewersHeraldAction
// and both are needlessly complex. This logic should live in the normal
// transaction application pipeline. See T10967.
$reviewers = $object->getReviewers();
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
if ($is_blocking) {
$new_status = DifferentialReviewerStatus::STATUS_BLOCKING;
} else {
$new_status = DifferentialReviewerStatus::STATUS_ADDED;
}
$new_strength = DifferentialReviewerStatus::getStatusStrength(
$new_status);
$current = array();
foreach ($phids as $phid) {
if (!isset($reviewers[$phid])) {
continue;
}
// If we're applying a stronger status (usually, upgrading a reviewer
// into a blocking reviewer), skip this check so we apply the change.
$old_strength = DifferentialReviewerStatus::getStatusStrength(
$reviewers[$phid]->getReviewerStatus());
if ($old_strength <= $new_strength) {
continue;
}
$current[] = $phid;
}
$phids = array_diff($phids, $current);
if (!$phids) {
return null;
}
$phids = array_fuse($phids);
$value = array();
foreach ($phids as $phid) {
if ($is_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
return $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType($reviewers_type)
->setNewValue(
array(
'+' => $value,
));
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withPHIDs(array($object->getPHID()))
->needActiveDiffs(true)
->needReviewers(true)
->executeOne();
if (!$revision) {
throw new Exception(
pht('Failed to load revision for Herald adapter construction!'));
}
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
$revision,
$revision->getActiveDiff());
// If the object is still a draft, prevent "Send me an email" and other
// similar rules from acting yet.
if (!$object->getShouldBroadcast()) {
$adapter->setForbiddenAction(
HeraldMailableState::STATECONST,
DifferentialHeraldStateReasons::REASON_DRAFT);
}
// If this edit didn't actually change the diff (for example, a user
// edited the title or changed subscribers), prevent "Run build plan"
// and other similar rules from acting yet, since the build results will
// not (or, at least, should not) change unless the actual source changes.
// We also don't run Differential builds if the update was caused by
// discovering a commit, as the expectation is that Diffusion builds take
// over once things land.
$has_update = false;
$has_commit = false;
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() != $type_update) {
continue;
}
if ($xaction->getMetadataValue('isCommitUpdate')) {
$has_commit = true;
} else {
$has_update = true;
}
break;
}
if ($has_commit) {
$adapter->setForbiddenAction(
HeraldBuildableState::STATECONST,
DifferentialHeraldStateReasons::REASON_LANDED);
} else if (!$has_update) {
$adapter->setForbiddenAction(
HeraldBuildableState::STATECONST,
DifferentialHeraldStateReasons::REASON_UNCHANGED);
}
return $adapter;
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private function updateAffectedPathTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$repository = $revision->getRepository();
if (!$repository) {
// The repository where the code lives is untracked.
return;
}
$path_prefix = null;
$local_root = $diff->getSourceControlPath();
if ($local_root) {
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root = id(new PhutilURI($local_root))->getPath();
$local_root = rtrim($local_root, '/');
$repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
$repo_root = rtrim($repo_root, '/');
if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
$path_prefix = substr($local_root, strlen($repo_root));
}
}
$changesets = $diff->getChangesets();
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
- // Save the affected paths; we'll use them later to query Owners. This
- // uses the un-expanded paths.
- $this->affectedPaths = $paths;
+ // If this change affected paths, save the changesets so we can apply
+ // Owners rules to them later.
+ if ($paths) {
+ $this->ownersDiff = $diff;
+ $this->ownersChangesets = $changesets;
+ }
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();
foreach ($paths as $local) {
foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
$all_paths[$path] = true;
}
}
$all_paths = array_keys($all_paths);
$path_ids =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
$all_paths);
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($path_ids as $path_id) {
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d)',
$repository->getID(),
$path_id,
time(),
$revision->getID());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$table->getTableName(),
$revision->getID());
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
$table->getTableName(),
implode(', ', $chunk));
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private function updateRevisionHashTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$vcs = $diff->getSourceControlSystem();
if ($vcs == DifferentialRevisionControlSystem::SVN) {
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return;
}
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff->getID(),
'local:commits');
if (!$property) {
return;
}
$hashes = array();
$data = $property->getData();
switch ($vcs) {
case DifferentialRevisionControlSystem::GIT:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit['rev'],
);
}
break;
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($hashes as $info) {
list($type, $hash) = $info;
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s)',
$revision->getID(),
$type,
$hash);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
ArcanistDifferentialRevisionHash::TABLE_NAME,
implode(', ', $sql));
}
}
private function renderAffectedFilesForMail(DifferentialDiff $diff) {
$changesets = $diff->getChangesets();
$filenames = mpull($changesets, 'getDisplayFilename');
sort($filenames);
$count = count($filenames);
$max = 250;
if ($count > $max) {
$filenames = array_slice($filenames, 0, $max);
$filenames[] = pht('(%d more files...)', ($count - $max));
}
return implode("\n", $filenames);
}
private function renderPatchHTMLForMail($patch) {
return phutil_tag('pre',
array('style' => 'font-family: monospace;'), $patch);
}
- private function buildPatchForMail(DifferentialDiff $diff) {
+ private function buildPatchForMail(DifferentialDiff $diff, $byte_limit) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
return id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
+ ->setByteLimit($byte_limit)
->buildPatch();
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// Reload to pick up the active diff and reviewer status.
return id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
}
protected function getCustomWorkerState() {
return array(
'changedPriorToCommitURI' => $this->changedPriorToCommitURI,
'firstBroadcast' => $this->firstBroadcast,
'isDraftDemotion' => $this->isDraftDemotion,
);
}
protected function loadCustomWorkerState(array $state) {
$this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI');
$this->firstBroadcast = idx($state, 'firstBroadcast');
$this->isDraftDemotion = idx($state, 'isDraftDemotion');
return $this;
}
private function newCommandeerReviewerTransaction(
DifferentialRevision $revision) {
$actor_phid = $this->getActingAsPHID();
$owner_phid = $revision->getAuthorPHID();
// If the user is commandeering, add the previous owner as a
// reviewer and remove the actor.
$edits = array(
'-' => array(
$actor_phid,
),
'+' => array(
$owner_phid,
),
);
// NOTE: We're setting setIsCommandeerSideEffect() on this because normally
// you can't add a revision's author as a reviewer, but this action swaps
// them after validation executes.
$xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
return id(new DifferentialTransaction())
->setTransactionType($xaction_type)
->setIgnoreOnNoEffect(true)
->setIsCommandeerSideEffect(true)
->setNewValue($edits);
}
public function getActiveDiff($object) {
if ($this->getIsNewObject()) {
return null;
} else {
return $object->getActiveDiff();
}
}
/**
* When a reviewer makes a comment, mark the last revision they commented
* on.
*
* This allows us to show a hint to help authors and other reviewers quickly
* distinguish between reviewers who have participated in the discussion and
* reviewers who haven't been part of it.
*/
private function markReviewerComments($object, array $xactions) {
$acting_phid = $this->getActingAsPHID();
if (!$acting_phid) {
return;
}
$diff = $this->getActiveDiff($object);
if (!$diff) {
return;
}
$has_comment = false;
foreach ($xactions as $xaction) {
if ($xaction->hasComment()) {
$has_comment = true;
break;
}
}
if (!$has_comment) {
return;
}
$reviewer_table = new DifferentialReviewer();
$conn = $reviewer_table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET lastCommentDiffPHID = %s
WHERE revisionPHID = %s
AND reviewerPHID = %s',
$reviewer_table->getTableName(),
$diff->getPHID(),
$object->getPHID(),
$acting_phid);
}
private function loadUnbroadcastTransactions($object) {
$viewer = $this->requireActor();
$xactions = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
return array_reverse($xactions);
}
protected function didApplyTransactions($object, array $xactions) {
// In a moment, we're going to try to publish draft revisions which have
// completed all their builds. However, we only want to do that if the
// actor is either the revision author or an omnipotent user (generally,
// the Harbormaster application).
// If we let any actor publish the revision as a side effect of other
// changes then an unlucky third party who innocently comments on the draft
// can end up racing Harbormaster and promoting the revision. At best, this
// is confusing. It can also run into validation problems with the "Request
// Review" transaction. See PHI309 for some discussion.
$author_phid = $object->getAuthorPHID();
$viewer = $this->requireActor();
$can_undraft =
($this->getActingAsPHID() === $author_phid) ||
($viewer->isOmnipotent());
// If a draft revision has no outstanding builds and we're automatically
// making drafts public after builds finish, make the revision public.
if ($can_undraft) {
$auto_undraft = !$object->getHoldAsDraft();
} else {
$auto_undraft = false;
}
- if ($object->isDraft() && $auto_undraft) {
+ $can_promote = false;
+ $can_demote = false;
+
+ // "Draft" revisions can promote to "Review Requested" after builds pass,
+ // or demote to "Changes Planned" after builds fail.
+ if ($object->isDraft()) {
+ $can_promote = true;
+ $can_demote = true;
+ }
+
+ // See PHI584. "Changes Planned" revisions which are not yet broadcasting
+ // can promote to "Review Requested" if builds pass.
+
+ // This pass is presumably the result of someone restarting the builds and
+ // having them work this time, perhaps because the builds are not perfectly
+ // reliable or perhaps because someone fixed some issue with build hardware
+ // or some other dependency.
+
+ // Currently, there's no legitimate way to end up in this state except
+ // through automatic demotion, so this behavior should not generate an
+ // undue level of confusion or ambiguity. Also note that these changes can
+ // not demote again since they've already been demoted once.
+ if ($object->isChangePlanned()) {
+ if (!$object->getShouldBroadcast()) {
+ $can_promote = true;
+ }
+ }
+
+ if (($can_promote || $can_demote) && $auto_undraft) {
$status = $this->loadCompletedBuildableStatus($object);
$is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED);
$is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED);
- if ($is_passed) {
+ if ($is_passed && $can_promote) {
// When Harbormaster moves a revision out of the draft state, we
// attribute the action to the revision author since this is more
// natural and more useful.
// Additionally, we change the acting PHID for the transaction set
// to the author if it isn't already a user so that mail comes from
// the natural author.
$acting_phid = $this->getActingAsPHID();
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($acting_phid) != $user_type) {
$this->setActingAsPHID($author_phid);
}
$xaction = $object->getApplicationTransactionTemplate()
->setAuthorPHID($author_phid)
->setTransactionType(
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)
->setNewValue(true);
// If we're creating this revision and immediately moving it out of
// the draft state, mark this as a create transaction so it gets
// hidden in the timeline and mail, since it isn't interesting: it
// is as though the draft phase never happened.
if ($this->getIsNewObject()) {
$xaction->setIsCreateTransaction(true);
}
// Queue this transaction and apply it separately after the current
// batch of transactions finishes so that Herald can fire on the new
// revision state. See T13027 for discussion.
$this->queueTransaction($xaction);
- } else if ($is_failed) {
+ } else if ($is_failed && $can_demote) {
// When demoting a revision, we act as "Harbormaster" instead of
// the author since this feels a little more natural.
$harbormaster_phid = id(new PhabricatorHarbormasterApplication())
->getPHID();
$xaction = $object->getApplicationTransactionTemplate()
->setAuthorPHID($harbormaster_phid)
->setMetadataValue('draft.demote', true)
->setTransactionType(
DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$this->queueTransaction($xaction);
-
- // TODO: Notify the author (only) that we did this.
}
}
// If the revision is new or was a draft, and is no longer a draft, we
// might be sending the first email about it.
// This might mean it was created directly into a non-draft state, or
// it just automatically undrafted after builds finished, or a user
// explicitly promoted it out of the draft state with an action like
// "Request Review".
// If we haven't sent any email about it yet, mark this email as the first
// email so the mail gets enriched with "SUMMARY" and "TEST PLAN".
$is_new = $this->getIsNewObject();
$was_broadcasting = $this->wasBroadcasting;
if ($object->getShouldBroadcast()) {
if (!$was_broadcasting || $is_new) {
// Mark this as the first broadcast we're sending about the revision
// so mail can generate specially.
$this->firstBroadcast = true;
}
}
return $xactions;
}
private function loadCompletedBuildableStatus(
DifferentialRevision $revision) {
$viewer = $this->requireActor();
$builds = $revision->loadImpactfulBuilds($viewer);
return $revision->newBuildableStatusForBuilds($builds);
}
private function requireReviewers(DifferentialRevision $revision) {
if ($revision->hasAttachedReviewers()) {
return;
}
$with_reviewers = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->withPHIDs(array($revision->getPHID()))
->executeOne();
if (!$with_reviewers) {
throw new Exception(
pht(
'Failed to reload revision ("%s").',
$revision->getPHID()));
}
$revision->attachReviewers($with_reviewers->getReviewers());
}
}
diff --git a/src/applications/differential/engine/DifferentialChangesetEngine.php b/src/applications/differential/engine/DifferentialChangesetEngine.php
new file mode 100644
index 000000000..e8a55a1b0
--- /dev/null
+++ b/src/applications/differential/engine/DifferentialChangesetEngine.php
@@ -0,0 +1,248 @@
+<?php
+
+final class DifferentialChangesetEngine extends Phobject {
+
+ public function rebuildChangesets(array $changesets) {
+ assert_instances_of($changesets, 'DifferentialChangeset');
+
+ foreach ($changesets as $changeset) {
+ $this->detectGeneratedCode($changeset);
+ $this->computeHashes($changeset);
+ }
+
+ $this->detectCopiedCode($changesets);
+ }
+
+
+/* -( Generated Code )----------------------------------------------------- */
+
+
+ private function detectGeneratedCode(DifferentialChangeset $changeset) {
+ $is_generated_trusted = $this->isTrustedGeneratedCode($changeset);
+ if ($is_generated_trusted) {
+ $changeset->setTrustedChangesetAttribute(
+ DifferentialChangeset::ATTRIBUTE_GENERATED,
+ $is_generated_trusted);
+ }
+
+ $is_generated_untrusted = $this->isUntrustedGeneratedCode($changeset);
+ if ($is_generated_untrusted) {
+ $changeset->setUntrustedChangesetAttribute(
+ DifferentialChangeset::ATTRIBUTE_GENERATED,
+ $is_generated_untrusted);
+ }
+ }
+
+ private function isTrustedGeneratedCode(DifferentialChangeset $changeset) {
+
+ $filename = $changeset->getFilename();
+
+ $paths = PhabricatorEnv::getEnvConfig('differential.generated-paths');
+ foreach ($paths as $regexp) {
+ if (preg_match($regexp, $filename)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function isUntrustedGeneratedCode(DifferentialChangeset $changeset) {
+
+ if ($changeset->getHunks()) {
+ $new_data = $changeset->makeNewFile();
+ if (strpos($new_data, '@'.'generated') !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+/* -( Content Hashes )----------------------------------------------------- */
+
+
+ private function computeHashes(DifferentialChangeset $changeset) {
+
+ $effect_key = DifferentialChangeset::METADATA_EFFECT_HASH;
+
+ $effect_hash = $this->newEffectHash($changeset);
+ if ($effect_hash !== null) {
+ $changeset->setChangesetMetadata($effect_key, $effect_hash);
+ }
+ }
+
+ private function newEffectHash(DifferentialChangeset $changeset) {
+
+ if ($changeset->getHunks()) {
+ $new_data = $changeset->makeNewFile();
+ return PhabricatorHash::digestForIndex($new_data);
+ }
+
+ return null;
+ }
+
+
+/* -( Copied Code )-------------------------------------------------------- */
+
+
+ private function detectCopiedCode(array $changesets) {
+ $min_width = 30;
+ $min_lines = 3;
+
+ $map = array();
+ $files = array();
+ $types = array();
+ foreach ($changesets as $changeset) {
+ $file = $changeset->getFilename();
+ foreach ($changeset->getHunks() as $hunk) {
+ $lines = $hunk->getStructuredOldFile();
+ foreach ($lines as $line => $info) {
+ $type = $info['type'];
+ if ($type == '\\') {
+ continue;
+ }
+ $types[$file][$line] = $type;
+
+ $text = $info['text'];
+ $text = trim($text);
+ $files[$file][$line] = $text;
+
+ if (strlen($text) >= $min_width) {
+ $map[$text][] = array($file, $line);
+ }
+ }
+ }
+ }
+
+ foreach ($changesets as $changeset) {
+ $copies = array();
+ foreach ($changeset->getHunks() as $hunk) {
+ $added = $hunk->getStructuredNewFile();
+ $atype = array();
+
+ foreach ($added as $line => $info) {
+ $atype[$line] = $info['type'];
+ $added[$line] = trim($info['text']);
+ }
+
+ $skip_lines = 0;
+ foreach ($added as $line => $code) {
+ if ($skip_lines) {
+ // We're skipping lines that we already processed because we
+ // extended a block above them downward to include them.
+ $skip_lines--;
+ continue;
+ }
+
+ if ($atype[$line] !== '+') {
+ // This line hasn't been changed in the new file, so don't try
+ // to figure out where it came from.
+ continue;
+ }
+
+ if (empty($map[$code])) {
+ // This line was too short to trigger copy/move detection.
+ continue;
+ }
+
+ if (count($map[$code]) > 16) {
+ // If there are a large number of identical lines in this diff,
+ // don't try to figure out where this block came from: the analysis
+ // is O(N^2), since we need to compare every line against every
+ // other line. Even if we arrive at a result, it is unlikely to be
+ // meaningful. See T5041.
+ continue;
+ }
+
+ $best_length = 0;
+
+ // Explore all candidates.
+ foreach ($map[$code] as $val) {
+ list($file, $orig_line) = $val;
+ $length = 1;
+
+ // Search backward and forward to find all of the adjacent lines
+ // which match.
+ foreach (array(-1, 1) as $direction) {
+ $offset = $direction;
+ while (true) {
+ if (isset($copies[$line + $offset])) {
+ // If we run into a block above us which we've already
+ // attributed to a move or copy from elsewhere, stop
+ // looking.
+ break;
+ }
+
+ if (!isset($added[$line + $offset])) {
+ // If we've run off the beginning or end of the new file,
+ // stop looking.
+ break;
+ }
+
+ if (!isset($files[$file][$orig_line + $offset])) {
+ // If we've run off the beginning or end of the original
+ // file, we also stop looking.
+ break;
+ }
+
+ $old = $files[$file][$orig_line + $offset];
+ $new = $added[$line + $offset];
+ if ($old !== $new) {
+ // If the old line doesn't match the new line, stop
+ // looking.
+ break;
+ }
+
+ $length++;
+ $offset += $direction;
+ }
+ }
+
+ if ($length < $best_length) {
+ // If we already know of a better source (more matching lines)
+ // for this move/copy, stick with that one. We prefer long
+ // copies/moves which match a lot of context over short ones.
+ continue;
+ }
+
+ if ($length == $best_length) {
+ if (idx($types[$file], $orig_line) != '-') {
+ // If we already know of an equally good source (same number
+ // of matching lines) and this isn't a move, stick with the
+ // other one. We prefer moves over copies.
+ continue;
+ }
+ }
+
+ $best_length = $length;
+ // ($offset - 1) contains number of forward matching lines.
+ $best_offset = $offset - 1;
+ $best_file = $file;
+ $best_line = $orig_line;
+ }
+
+ $file = ($best_file == $changeset->getFilename() ? '' : $best_file);
+ for ($i = $best_length; $i--; ) {
+ $type = idx($types[$best_file], $best_line + $best_offset - $i);
+ $copies[$line + $best_offset - $i] = ($best_length < $min_lines
+ ? array() // Ignore short blocks.
+ : array($file, $best_line + $best_offset - $i, $type));
+ }
+
+ $skip_lines = $best_offset;
+ }
+ }
+
+ $copies = array_filter($copies);
+ if ($copies) {
+ $metadata = $changeset->getMetadata();
+ $metadata['copy:lines'] = $copies;
+ $changeset->setMetadata($metadata);
+ }
+ }
+
+ }
+
+}
diff --git a/src/applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php b/src/applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php
new file mode 100644
index 000000000..395541c9f
--- /dev/null
+++ b/src/applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php
@@ -0,0 +1,77 @@
+<?php
+
+final class DifferentialCommitsSearchEngineAttachment
+ extends PhabricatorSearchEngineAttachment {
+
+ public function getAttachmentName() {
+ return pht('Diff Commits');
+ }
+
+ public function getAttachmentDescription() {
+ return pht('Get the local commits (if any) for each diff.');
+ }
+
+ public function loadAttachmentData(array $objects, $spec) {
+ $properties = id(new DifferentialDiffProperty())->loadAllWhere(
+ 'diffID IN (%Ld) AND name = %s',
+ mpull($objects, 'getID'),
+ 'local:commits');
+
+ $map = array();
+ foreach ($properties as $property) {
+ $map[$property->getDiffID()] = $property->getData();
+ }
+
+ return $map;
+ }
+
+ public function getAttachmentForObject($object, $data, $spec) {
+ $diff_id = $object->getID();
+ $info = idx($data, $diff_id, array());
+
+ // NOTE: This should be similar to the information returned about commits
+ // by "diffusion.commit.search".
+
+ $list = array();
+ foreach ($info as $commit) {
+ $author_epoch = idx($commit, 'time');
+ if ($author_epoch) {
+ $author_epoch = (int)$author_epoch;
+ }
+
+ // TODO: Currently, we don't upload the raw author string from "arc".
+ // Reconstruct a plausible version of it until we begin uploading this
+ // information.
+
+ $author_name = idx($commit, 'author');
+ $author_email = idx($commit, 'authorEmail');
+ if (strlen($author_name) && strlen($author_email)) {
+ $author_raw = (string)id(new PhutilEmailAddress())
+ ->setDisplayName($author_name)
+ ->setAddress($author_email);
+ } else if (strlen($author_email)) {
+ $author_raw = $author_email;
+ } else {
+ $author_raw = $author_name;
+ }
+
+ $list[] = array(
+ 'identifier' => $commit['commit'],
+ 'tree' => idx($commit, 'tree'),
+ 'parents' => idx($commit, 'parents', array()),
+ 'author' => array(
+ 'name' => $author_name,
+ 'email' => $author_email,
+ 'raw' => $author_raw,
+ 'epoch' => $author_epoch,
+ ),
+ 'message' => idx($commit, 'message'),
+ );
+ }
+
+ return array(
+ 'commits' => $list,
+ );
+ }
+
+}
diff --git a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
index df38885ed..8554f7be2 100644
--- a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
+++ b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
@@ -1,57 +1,81 @@
<?php
final class DifferentialBuildableEngine
extends HarbormasterBuildableEngine {
protected function getPublishableObject() {
$object = $this->getObject();
if ($object instanceof DifferentialDiff) {
return $object->getRevision();
}
return $object;
}
public function publishBuildable(
HarbormasterBuildable $old,
HarbormasterBuildable $new) {
// If we're publishing to a diff that is not actually attached to a
// revision, we have nothing to publish to, so just bail out.
$revision = $this->getPublishableObject();
if (!$revision) {
return;
}
// Don't publish manual buildables.
if ($new->getIsManualBuildable()) {
return;
}
// Don't publish anything if the buildable is still building. Differential
// treats more buildables as "building" than Harbormaster does, but the
// Differential definition is a superset of the Harbormaster definition.
if ($new->isBuilding()) {
return;
}
$viewer = $this->getViewer();
$old_status = $revision->getBuildableStatus($new->getPHID());
$new_status = $revision->newBuildableStatus($viewer, $new->getPHID());
if ($old_status === $new_status) {
return;
}
$buildable_type = DifferentialRevisionBuildableTransaction::TRANSACTIONTYPE;
$xaction = $this->newTransaction()
->setMetadataValue('harbormaster:buildablePHID', $new->getPHID())
->setTransactionType($buildable_type)
->setNewValue($new_status);
$this->applyTransactions(array($xaction));
}
+ public function getAuthorIdentity() {
+ $object = $this->getObject();
+
+ if ($object instanceof DifferentialRevision) {
+ $object = $object->loadActiveDiff();
+ }
+
+ $authorship = $object->getDiffAuthorshipDict();
+ if (!isset($authorship['authorName'])) {
+ return null;
+ }
+
+ $name = $authorship['authorName'];
+ $address = idx($authorship, 'authorEmail');
+
+ $full = id(new PhutilEmailAddress())
+ ->setDisplayName($name)
+ ->setAddress($address);
+
+ return id(new PhabricatorRepositoryIdentity())
+ ->setIdentityName((string)$full)
+ ->makeEphemeral();
+ }
+
}
diff --git a/src/applications/differential/herald/DifferentialRevisionSummaryHeraldField.php b/src/applications/differential/herald/DifferentialRevisionSummaryHeraldField.php
index eafc9bc99..587468d54 100644
--- a/src/applications/differential/herald/DifferentialRevisionSummaryHeraldField.php
+++ b/src/applications/differential/herald/DifferentialRevisionSummaryHeraldField.php
@@ -1,23 +1,20 @@
<?php
final class DifferentialRevisionSummaryHeraldField
extends DifferentialRevisionHeraldField {
const FIELDCONST = 'differential.revision.summary';
public function getHeraldFieldName() {
return pht('Revision summary');
}
public function getHeraldFieldValue($object) {
- // NOTE: For historical reasons, this field includes the test plan. We
- // could maybe try to fix this some day, but it probably aligns reasonably
- // well with user expectation without harming anything.
- return $object->getSummary()."\n\n".$object->getTestPlan();
+ return $object->getSummary();
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_TEXT;
}
}
diff --git a/src/applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php b/src/applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php
new file mode 100644
index 000000000..bb2e6d091
--- /dev/null
+++ b/src/applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php
@@ -0,0 +1,20 @@
+<?php
+
+final class DifferentialRevisionTestPlanHeraldField
+ extends DifferentialRevisionHeraldField {
+
+ const FIELDCONST = 'differential.revision.test-plan';
+
+ public function getHeraldFieldName() {
+ return pht('Revision test plan');
+ }
+
+ public function getHeraldFieldValue($object) {
+ return $object->getTestPlan();
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_TEXT;
+ }
+
+}
diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php
index 9f7f8fe3f..858a42dbc 100644
--- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php
@@ -1,157 +1,158 @@
<?php
final class HeraldDifferentialRevisionAdapter
extends HeraldDifferentialAdapter
implements HarbormasterBuildableAdapterInterface {
protected $revision;
protected $affectedPackages;
protected $changesets;
private $haveHunks;
private $buildRequests = array();
public function getAdapterApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function newObject() {
return new DifferentialRevision();
}
public function isTestAdapterForObject($object) {
return ($object instanceof DifferentialRevision);
}
public function getAdapterTestDescription() {
return pht(
'Test rules which run when a revision is created or updated.');
}
public function newTestAdapter(PhabricatorUser $viewer, $object) {
return self::newLegacyAdapter(
$object,
$object->loadActiveDiff());
}
protected function initializeNewAdapter() {
$this->revision = $this->newObject();
}
public function getObject() {
return $this->revision;
}
public function getAdapterContentType() {
return 'differential';
}
public function getAdapterContentName() {
return pht('Differential Revisions');
}
public function getAdapterContentDescription() {
return pht(
"React to revisions being created or updated.\n".
"Revision rules can send email, flag revisions, add reviewers, ".
"and run build plans.");
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return true;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
default:
return false;
}
}
public static function newLegacyAdapter(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$object = new HeraldDifferentialRevisionAdapter();
// Reload the revision to pick up relationship information.
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($revision->getID()))
->setViewer(PhabricatorUser::getOmnipotentUser())
->needReviewers(true)
->executeOne();
$object->revision = $revision;
$object->setDiff($diff);
return $object;
}
public function getHeraldName() {
return $this->revision->getTitle();
}
protected function loadChangesets() {
if ($this->changesets === null) {
$this->changesets = $this->getDiff()->loadChangesets();
}
return $this->changesets;
}
protected function loadChangesetsWithHunks() {
$changesets = $this->loadChangesets();
if ($changesets && !$this->haveHunks) {
$this->haveHunks = true;
id(new DifferentialHunkQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
}
return $changesets;
}
public function loadAffectedPackages() {
if ($this->affectedPackages === null) {
$this->affectedPackages = array();
$repository = $this->loadRepository();
if ($repository) {
- $packages = PhabricatorOwnersPackage::loadAffectedPackages(
+ $packages = PhabricatorOwnersPackage::loadAffectedPackagesForChangesets(
$repository,
- $this->loadAffectedPaths());
+ $this->getDiff(),
+ $this->loadChangesets());
$this->affectedPackages = $packages;
}
}
return $this->affectedPackages;
}
public function loadReviewers() {
return $this->getObject()->getReviewerPHIDs();
}
/* -( HarbormasterBuildableAdapterInterface )------------------------------ */
public function getHarbormasterBuildablePHID() {
return $this->getDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getObject()->getPHID();
}
public function getQueuedHarbormasterBuildRequests() {
return $this->buildRequests;
}
public function queueHarbormasterBuildRequest(
HarbormasterBuildRequest $request) {
$this->buildRequests[] = $request;
}
}
diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
index 2443fe965..26632dff2 100644
--- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
+++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
@@ -1,107 +1,109 @@
<?php
final class PhabricatorDifferentialRevisionTestDataGenerator
extends PhabricatorTestDataGenerator {
const GENERATORKEY = 'revisions';
public function getGeneratorName() {
return pht('Differential Revisions');
}
public function generateObject() {
$author = $this->loadPhabricatorUser();
$revision = DifferentialRevision::initializeNewRevision($author);
$revision->attachReviewers(array());
$revision->attachActiveDiff(null);
// This could be a bit richer and more formal than it is.
$revision->setTitle($this->generateTitle());
$revision->setSummary($this->generateDescription());
$revision->setTestPlan($this->generateDescription());
$diff = $this->generateDiff($author);
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
$xactions = array();
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($type_update)
->setNewValue($diff->getPHID());
id(new DifferentialTransactionEditor())
->setActor($author)
->setContentSource($this->getLipsumContentSource())
->applyTransactions($revision, $xactions);
return $revision;
}
public function getCCPHIDs() {
$ccs = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$ccs[] = $this->loadPhabricatorUserPHID();
}
return $ccs;
}
public function generateDiff($author) {
$paste_generator = new PhabricatorPasteTestDataGenerator();
- $languages = $paste_generator->supportedLanguages;
- $lang = array_rand($languages);
- $code = $paste_generator->generateContent($lang);
- $altcode = $paste_generator->generateContent($lang);
+ $languages = $paste_generator->getSupportedLanguages();
+ $language = array_rand($languages);
+ $spec = $languages[$language];
+
+ $code = $paste_generator->generateContent($spec);
+ $altcode = $paste_generator->generateContent($spec);
$newcode = $this->randomlyModify($code, $altcode);
$diff = id(new PhabricatorDifferenceEngine())
->generateRawDiffFromFileContent($code, $newcode);
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $diff,
));
$call->setUser($author);
$result = $call->execute();
$thediff = id(new DifferentialDiff())->load(
$result['id']);
$thediff->setDescription($this->generateTitle())->save();
return $thediff;
}
public function generateDescription() {
return id(new PhutilLipsumContextFreeGrammar())
->generate(10, 20);
}
public function generateTitle() {
return id(new PhutilLipsumContextFreeGrammar())
->generate();
}
public function randomlyModify($code, $altcode) {
$codearr = explode("\n", $code);
$altcodearr = explode("\n", $altcode);
$no_lines_to_delete = rand(1,
min(count($codearr) - 2, 5));
$randomlines = array_rand($codearr,
count($codearr) - $no_lines_to_delete);
$newcode = array();
foreach ($randomlines as $lineno) {
$newcode[] = $codearr[$lineno];
}
$newlines_count = rand(2,
min(count($codearr) - 2, count($altcodearr) - 2, 5));
$randomlines_orig = array_rand($codearr, $newlines_count);
$randomlines_new = array_rand($altcodearr, $newlines_count);
$newcode2 = array();
$c = 0;
for ($i = 0; $i < count($newcode);$i++) {
$newcode2[] = $newcode[$i];
if (in_array($i, $randomlines_orig)) {
$newcode2[] = $altcodearr[$randomlines_new[$c++]];
}
}
return implode($newcode2, "\n");
}
}
diff --git a/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php b/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php
index 99125645e..0db158e17 100644
--- a/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php
+++ b/src/applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php
@@ -1,86 +1,212 @@
<?php
final class PhabricatorDifferentialMigrateHunkWorkflow
extends PhabricatorDifferentialManagementWorkflow {
protected function didConstruct() {
$this
->setName('migrate-hunk')
- ->setExamples('**migrate-hunk** --id __hunk__ --to __storage__')
+ ->setExamples(
+ "**migrate-hunk** --id __hunk__ --to __storage__\n".
+ "**migrate-hunk** --all")
->setSynopsis(pht('Migrate storage engines for a hunk.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('Hunk ID to migrate.'),
),
array(
'name' => 'to',
'param' => 'storage',
'help' => pht('Storage engine to migrate to.'),
),
+ array(
+ 'name' => 'all',
+ 'help' => pht('Migrate all hunks.'),
+ ),
+ array(
+ 'name' => 'auto',
+ 'help' => pht('Select storage format automatically.'),
+ ),
+ array(
+ 'name' => 'dry-run',
+ 'help' => pht('Show planned writes but do not perform them.'),
+ ),
));
}
public function execute(PhutilArgumentParser $args) {
+ $is_dry_run = $args->getArg('dry-run');
+
$id = $args->getArg('id');
- if (!$id) {
+ $is_all = $args->getArg('all');
+
+ if ($is_all && $id) {
throw new PhutilArgumentUsageException(
- pht('Specify a hunk to migrate with --id.'));
+ pht(
+ 'Options "--all" (to migrate all hunks) and "--id" (to migrate a '.
+ 'specific hunk) are mutually exclusive.'));
+ } else if (!$is_all && !$id) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify a hunk to migrate with "--id", or migrate all hunks '.
+ 'with "--all".'));
}
+ $is_auto = $args->getArg('auto');
$storage = $args->getArg('to');
- switch ($storage) {
- case DifferentialHunk::DATATYPE_TEXT:
- case DifferentialHunk::DATATYPE_FILE:
- break;
- default:
- throw new PhutilArgumentUsageException(
- pht('Specify a hunk storage engine with --to.'));
+ if ($is_auto && $storage) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Options "--to" (to choose a specific storage format) and "--auto" '.
+ '(to select a storage format automatically) are mutually '.
+ 'exclusive.'));
+ } else if (!$is_auto && !$storage) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Use "--to" to choose a storage format, or "--auto" to select a '.
+ 'format automatically.'));
}
- $hunk = $this->loadHunk($id);
- $old_data = $hunk->getChanges();
+ $types = array(
+ DifferentialHunk::DATATYPE_TEXT,
+ DifferentialHunk::DATATYPE_FILE,
+ );
+ $types = array_fuse($types);
+ if (strlen($storage)) {
+ if (!isset($types[$storage])) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Storage type "%s" is unknown. Supported types are: %s.',
+ $storage,
+ implode(', ', array_keys($types))));
+ }
+ }
- switch ($storage) {
- case DifferentialHunk::DATATYPE_TEXT:
- $hunk->saveAsText();
- $this->logOkay(
- pht('TEXT'),
- pht('Convereted hunk to text storage.'));
- break;
- case DifferentialHunk::DATATYPE_FILE:
- $hunk->saveAsFile();
- $this->logOkay(
- pht('FILE'),
- pht('Convereted hunk to file storage.'));
- break;
+ if ($id) {
+ $hunk = $this->loadHunk($id);
+ $hunks = array($hunk);
+ } else {
+ $hunks = new LiskMigrationIterator(new DifferentialHunk());
}
- $hunk = $this->loadHunk($id);
- $new_data = $hunk->getChanges();
+ foreach ($hunks as $hunk) {
+ try {
+ $this->migrateHunk($hunk, $storage, $is_auto, $is_dry_run);
+ } catch (Exception $ex) {
+ // If we're migrating a single hunk, just throw the exception. If
+ // we're migrating multiple hunks, warn but continue.
+ if ($id) {
+ throw $ex;
+ }
- if ($old_data !== $new_data) {
- throw new Exception(
- pht(
- 'Integrity check failed: new file data differs fom old data!'));
+ $this->logWarn(
+ pht('WARN'),
+ pht(
+ 'Failed to migrate hunk %d: %s',
+ $hunk->getID(),
+ $ex->getMessage()));
+ }
}
return 0;
}
private function loadHunk($id) {
$hunk = id(new DifferentialHunk())->load($id);
if (!$hunk) {
throw new PhutilArgumentUsageException(
pht(
'No hunk exists with ID "%s".',
$id));
}
return $hunk;
}
+ private function migrateHunk(
+ DifferentialHunk $hunk,
+ $type,
+ $is_auto,
+ $is_dry_run) {
+
+ $old_type = $hunk->getDataType();
+
+ if ($is_auto) {
+ // By default, we're just going to keep hunks in the same storage
+ // engine. In the future, we could perhaps select large hunks stored in
+ // text engine and move them into file storage.
+ $new_type = $old_type;
+ } else {
+ $new_type = $type;
+ }
+
+ // Figure out if the storage format (e.g., plain text vs compressed)
+ // would change if we wrote this hunk anew today.
+ $old_format = $hunk->getDataFormat();
+ $new_format = $hunk->getAutomaticDataFormat();
+
+ $same_type = ($old_type === $new_type);
+ $same_format = ($old_format === $new_format);
+
+ // If we aren't going to change the storage engine and aren't going to
+ // change the storage format, just bail out.
+ if ($same_type && $same_format) {
+ $this->logInfo(
+ pht('SKIP'),
+ pht(
+ 'Hunk %d is already stored in the preferred engine ("%s") '.
+ 'with the preferred format ("%s").',
+ $hunk->getID(),
+ $new_type,
+ $new_format));
+ return;
+ }
+
+ if ($is_dry_run) {
+ $this->logOkay(
+ pht('DRY RUN'),
+ pht(
+ 'Hunk %d would be rewritten (storage: "%s" -> "%s"; '.
+ 'format: "%s" -> "%s").',
+ $hunk->getID(),
+ $old_type,
+ $new_type,
+ $old_format,
+ $new_format));
+ return;
+ }
+
+ $old_data = $hunk->getChanges();
+
+ switch ($new_type) {
+ case DifferentialHunk::DATATYPE_TEXT:
+ $hunk->saveAsText();
+ break;
+ case DifferentialHunk::DATATYPE_FILE:
+ $hunk->saveAsFile();
+ break;
+ }
+
+ $this->logOkay(
+ pht('MIGRATE'),
+ pht(
+ 'Converted hunk %d to "%s" storage (with format "%s").',
+ $hunk->getID(),
+ $new_type,
+ $hunk->getDataFormat()));
+
+ $hunk = $this->loadHunk($hunk->getID());
+ $new_data = $hunk->getChanges();
+
+ if ($old_data !== $new_data) {
+ throw new Exception(
+ pht(
+ 'Integrity check failed: new file data differs from old data!'));
+ }
+ }
+
}
diff --git a/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php b/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php
new file mode 100644
index 000000000..068771284
--- /dev/null
+++ b/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php
@@ -0,0 +1,96 @@
+<?php
+
+final class PhabricatorDifferentialRebuildChangesetsWorkflow
+ extends PhabricatorDifferentialManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('rebuild-changesets')
+ ->setExamples('**rebuild-changesets** --revision __revision__')
+ ->setSynopsis(pht('Rebuild changesets for a revision.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'revision',
+ 'param' => 'revision',
+ 'help' => pht('Revision to rebuild changesets for.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $revision_identifier = $args->getArg('revision');
+ if (!$revision_identifier) {
+ throw new PhutilArgumentUsageException(
+ pht('Specify a revision to rebuild changesets for with "--revision".'));
+ }
+
+ $revision = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames(array($revision_identifier))
+ ->executeOne();
+ if ($revision) {
+ if (!($revision instanceof DifferentialRevision)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Object "%s" specified by "--revision" must be a Differential '.
+ 'revision.'));
+ }
+ } else {
+ $revision = id(new DifferentialRevisionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($revision_identifier))
+ ->executeOne();
+ }
+
+ if (!$revision) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'No revision "%s" exists.',
+ $revision_identifier));
+ }
+
+ $diffs = id(new DifferentialDiffQuery())
+ ->setViewer($viewer)
+ ->withRevisionIDs(array($revision->getID()))
+ ->execute();
+
+ $changesets = id(new DifferentialChangesetQuery())
+ ->setViewer($viewer)
+ ->withDiffs($diffs)
+ ->needHunks(true)
+ ->execute();
+
+ $changeset_groups = mgroup($changesets, 'getDiffID');
+
+ foreach ($changeset_groups as $diff_id => $changesets) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Rebuilding %s changeset(s) for diff ID %d.',
+ phutil_count($changesets),
+ $diff_id));
+
+ foreach ($changesets as $changeset) {
+ echo tsprintf(
+ " %s\n",
+ $changeset->getFilename());
+ }
+
+ id(new DifferentialChangesetEngine())
+ ->rebuildChangesets($changesets);
+
+ foreach ($changesets as $changeset) {
+ $changeset->save();
+ }
+
+ echo tsprintf(
+ "%s\n",
+ pht('Done.'));
+ }
+ }
+
+
+}
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index 1eeb4c845..596ea0e42 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,1638 +1,1483 @@
<?php
final class DifferentialChangesetParser extends Phobject {
const HIGHLIGHT_BYTE_LIMIT = 262144;
protected $visible = array();
protected $new = array();
protected $old = array();
protected $intra = array();
protected $newRender = null;
protected $oldRender = null;
protected $filename = null;
protected $hunkStartLines = array();
protected $comments = array();
protected $specialAttributes = array();
protected $changeset;
protected $whitespaceMode = null;
protected $renderCacheKey = null;
private $handles = array();
private $user;
private $leftSideChangesetID;
private $leftSideAttachesToNewFile;
private $rightSideChangesetID;
private $rightSideAttachesToNewFile;
private $originalLeft;
private $originalRight;
private $renderingReference;
private $isSubparser;
private $isTopLevel;
private $coverage;
private $markupEngine;
private $highlightErrors;
private $disableCache;
private $renderer;
private $characterEncoding;
private $highlightAs;
private $highlightingDisabled;
private $showEditAndReplyLinks = true;
private $canMarkDone;
private $objectOwnerPHID;
private $offsetMode;
private $rangeStart;
private $rangeEnd;
private $mask;
private $linesOfContext = 8;
private $highlightEngine;
public function setRange($start, $end) {
$this->rangeStart = $start;
$this->rangeEnd = $end;
return $this;
}
public function setMask(array $mask) {
$this->mask = $mask;
return $this;
}
public function renderChangeset() {
return $this->render($this->rangeStart, $this->rangeEnd, $this->mask);
}
public function setShowEditAndReplyLinks($bool) {
$this->showEditAndReplyLinks = $bool;
return $this;
}
public function getShowEditAndReplyLinks() {
return $this->showEditAndReplyLinks;
}
public function setHighlightAs($highlight_as) {
$this->highlightAs = $highlight_as;
return $this;
}
public function getHighlightAs() {
return $this->highlightAs;
}
public function setCharacterEncoding($character_encoding) {
$this->characterEncoding = $character_encoding;
return $this;
}
public function getCharacterEncoding() {
return $this->characterEncoding;
}
public function setRenderer(DifferentialChangesetRenderer $renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
if (!$this->renderer) {
return new DifferentialChangesetTwoUpRenderer();
}
return $this->renderer;
}
public function setDisableCache($disable_cache) {
$this->disableCache = $disable_cache;
return $this;
}
public function getDisableCache() {
return $this->disableCache;
}
public function setCanMarkDone($can_mark_done) {
$this->canMarkDone = $can_mark_done;
return $this;
}
public function getCanMarkDone() {
return $this->canMarkDone;
}
public function setObjectOwnerPHID($phid) {
$this->objectOwnerPHID = $phid;
return $this;
}
public function getObjectOwnerPHID() {
return $this->objectOwnerPHID;
}
public function setOffsetMode($offset_mode) {
$this->offsetMode = $offset_mode;
return $this;
}
public function getOffsetMode() {
return $this->offsetMode;
}
public static function getDefaultRendererForViewer(PhabricatorUser $viewer) {
$is_unified = $viewer->compareUserSetting(
PhabricatorUnifiedDiffsSetting::SETTINGKEY,
PhabricatorUnifiedDiffsSetting::VALUE_ALWAYS_UNIFIED);
if ($is_unified) {
return '1up';
}
return null;
}
public function readParametersFromRequest(AphrontRequest $request) {
$this->setWhitespaceMode($request->getStr('whitespace'));
$this->setCharacterEncoding($request->getStr('encoding'));
$this->setHighlightAs($request->getStr('highlight'));
$renderer = null;
// If the viewer prefers unified diffs, always set the renderer to unified.
// Otherwise, we leave it unspecified and the client will choose a
// renderer based on the screen size.
if ($request->getStr('renderer')) {
$renderer = $request->getStr('renderer');
} else {
$renderer = self::getDefaultRendererForViewer($request->getViewer());
}
switch ($renderer) {
case '1up':
$this->setRenderer(new DifferentialChangesetOneUpRenderer());
break;
default:
$this->setRenderer(new DifferentialChangesetTwoUpRenderer());
break;
}
return $this;
}
const CACHE_VERSION = 11;
const CACHE_MAX_SIZE = 8e6;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
const ATTR_WHITELINES = 'attr:white';
const ATTR_MOVEAWAY = 'attr:moveaway';
const WHITESPACE_SHOW_ALL = 'show-all';
const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
const WHITESPACE_IGNORE_MOST = 'ignore-most';
const WHITESPACE_IGNORE_ALL = 'ignore-all';
public function setOldLines(array $lines) {
$this->old = $lines;
return $this;
}
public function setNewLines(array $lines) {
$this->new = $lines;
return $this;
}
public function setSpecialAttributes(array $attributes) {
$this->specialAttributes = $attributes;
return $this;
}
public function setIntraLineDiffs(array $diffs) {
$this->intra = $diffs;
return $this;
}
public function setVisibileLinesMask(array $mask) {
$this->visible = $mask;
return $this;
}
public function setLinesOfContext($lines_of_context) {
$this->linesOfContext = $lines_of_context;
return $this;
}
public function getLinesOfContext() {
return $this->linesOfContext;
}
/**
* Configure which Changeset comments added to the right side of the visible
* diff will be attached to. The ID must be the ID of a real Differential
* Changeset.
*
* The complexity here is that we may show an arbitrary side of an arbitrary
* changeset as either the left or right part of a diff. This method allows
* the left and right halves of the displayed diff to be correctly mapped to
* storage changesets.
*
* @param id The Differential Changeset ID that comments added to the right
* side of the visible diff should be attached to.
* @param bool If true, attach new comments to the right side of the storage
* changeset. Note that this may be false, if the left side of
* some storage changeset is being shown as the right side of
* a display diff.
* @return this
*/
public function setRightSideCommentMapping($id, $is_new) {
$this->rightSideChangesetID = $id;
$this->rightSideAttachesToNewFile = $is_new;
return $this;
}
/**
* See setRightSideCommentMapping(), but this sets information for the left
* side of the display diff.
*/
public function setLeftSideCommentMapping($id, $is_new) {
$this->leftSideChangesetID = $id;
$this->leftSideAttachesToNewFile = $is_new;
return $this;
}
public function setOriginals(
DifferentialChangeset $left,
DifferentialChangeset $right) {
$this->originalLeft = $left;
$this->originalRight = $right;
return $this;
}
public function diffOriginals() {
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent(
implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
$parser = new DifferentialHunkParser();
return $parser->parseHunksForHighlightMasks(
$changeset->getHunks(),
$this->originalLeft->getHunks(),
$this->originalRight->getHunks());
}
/**
* Set a key for identifying this changeset in the render cache. If set, the
* parser will attempt to use the changeset render cache, which can improve
* performance for frequently-viewed changesets.
*
* By default, there is no render cache key and parsers do not use the cache.
* This is appropriate for rarely-viewed changesets.
*
* NOTE: Currently, this key must be a valid Differential Changeset ID.
*
* @param string Key for identifying this changeset in the render cache.
* @return this
*/
public function setRenderCacheKey($key) {
$this->renderCacheKey = $key;
return $this;
}
private function getRenderCacheKey() {
return $this->renderCacheKey;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
return $this;
}
public function setWhitespaceMode($whitespace_mode) {
$this->whitespaceMode = $whitespace_mode;
return $this;
}
public function setRenderingReference($ref) {
$this->renderingReference = $ref;
return $this;
}
private function getRenderingReference() {
return $this->renderingReference;
}
public function getChangeset() {
return $this->changeset;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
private function getCoverage() {
return $this->coverage;
}
public function parseInlineComment(
PhabricatorInlineCommentInterface $comment) {
// Parse only comments which are actually visible.
if ($this->isCommentVisibleOnRenderedDiff($comment)) {
$this->comments[] = $comment;
}
return $this;
}
private function loadCache() {
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$data = null;
$changeset = new DifferentialChangeset();
$conn_r = $changeset->establishConnection('r');
$data = queryfx_one(
$conn_r,
'SELECT * FROM %T WHERE id = %d',
$changeset->getTableName().'_parse_cache',
$render_cache_key);
if (!$data) {
return false;
}
if ($data['cache'][0] == '{') {
// This is likely an old-style JSON cache which we will not be able to
// deserialize.
return false;
}
$data = unserialize($data['cache']);
if (!is_array($data) || !$data) {
return false;
}
foreach (self::getCacheableProperties() as $cache_key) {
if (!array_key_exists($cache_key, $data)) {
// If we're missing a cache key, assume we're looking at an old cache
// and ignore it.
return false;
}
}
if ($data['cacheVersion'] !== self::CACHE_VERSION) {
return false;
}
// Someone displays contents of a partially cached shielded file.
if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
return false;
}
unset($data['cacheVersion'], $data['cacheHost']);
$cache_prop = array_select_keys($data, self::getCacheableProperties());
foreach ($cache_prop as $cache_key => $v) {
$this->$cache_key = $v;
}
return true;
}
protected static function getCacheableProperties() {
return array(
'visible',
'new',
'old',
'intra',
'newRender',
'oldRender',
'specialAttributes',
'hunkStartLines',
'cacheVersion',
'cacheHost',
'highlightingDisabled',
);
}
public function saveCache() {
if (PhabricatorEnv::isReadOnly()) {
return false;
}
if ($this->highlightErrors) {
return false;
}
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$cache = array();
foreach (self::getCacheableProperties() as $cache_key) {
switch ($cache_key) {
case 'cacheVersion':
$cache[$cache_key] = self::CACHE_VERSION;
break;
case 'cacheHost':
$cache[$cache_key] = php_uname('n');
break;
default:
$cache[$cache_key] = $this->$cache_key;
break;
}
}
$cache = serialize($cache);
// We don't want to waste too much space by a single changeset.
if (strlen($cache) > self::CACHE_MAX_SIZE) {
return;
}
$changeset = new DifferentialChangeset();
$conn_w = $changeset->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
queryfx(
$conn_w,
'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d)
ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
DifferentialChangeset::TABLE_CACHE,
$render_cache_key,
$cache,
time());
} catch (AphrontQueryException $ex) {
// Ignore these exceptions. A common cause is that the cache is
// larger than 'max_allowed_packet', in which case we're better off
// not writing it.
// TODO: It would be nice to tailor this more narrowly.
}
unset($unguarded);
}
private function markGenerated($new_corpus_block = '') {
$generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
if (!$generated_guess) {
$generated_path_regexps = PhabricatorEnv::getEnvConfig(
'differential.generated-paths');
foreach ($generated_path_regexps as $regexp) {
if (preg_match($regexp, $this->changeset->getFilename())) {
$generated_guess = true;
break;
}
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
array(
'corpus' => $new_corpus_block,
'is_generated' => $generated_guess,
)
);
PhutilEventEngine::dispatchEvent($event);
$generated = $event->getValue('is_generated');
+
+ $attribute = $this->changeset->isGeneratedChangeset();
+ if ($attribute) {
+ $generated = true;
+ }
+
$this->specialAttributes[self::ATTR_GENERATED] = $generated;
}
public function isGenerated() {
return idx($this->specialAttributes, self::ATTR_GENERATED, false);
}
public function isDeleted() {
return idx($this->specialAttributes, self::ATTR_DELETED, false);
}
public function isUnchanged() {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
public function isWhitespaceOnly() {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
public function isMoveAway() {
return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);
}
private function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
if (isset($intra[$key])) {
$render[$key] = ArcanistDiffUtils::applyIntralineDiff(
$text,
$intra[$key]);
}
}
}
private function getHighlightFuture($corpus) {
$language = $this->highlightAs;
if (!$language) {
$language = $this->highlightEngine->getLanguageFromFilename(
$this->filename);
if (($language != 'txt') &&
(strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) {
$this->highlightingDisabled = true;
$language = 'txt';
}
}
return $this->highlightEngine->getHighlightFuture(
$language,
$corpus);
}
protected function processHighlightedSource($data, $result) {
$result_lines = phutil_split_lines($result);
foreach ($data as $key => $info) {
if (!$info) {
unset($result_lines[$key]);
}
}
return $result_lines;
}
private function tryCacheStuff() {
$whitespace_mode = $this->whitespaceMode;
switch ($whitespace_mode) {
case self::WHITESPACE_SHOW_ALL:
case self::WHITESPACE_IGNORE_TRAILING:
case self::WHITESPACE_IGNORE_ALL:
break;
default:
$whitespace_mode = self::WHITESPACE_IGNORE_MOST;
break;
}
$skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST);
if ($this->disableCache) {
$skip_cache = true;
}
if ($this->characterEncoding) {
$skip_cache = true;
}
if ($this->highlightAs) {
$skip_cache = true;
}
$this->whitespaceMode = $whitespace_mode;
$changeset = $this->changeset;
if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
$changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
$this->markGenerated();
} else {
if ($skip_cache || !$this->loadCache()) {
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
private function process() {
$whitespace_mode = $this->whitespaceMode;
$changeset = $this->changeset;
$ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) ||
($whitespace_mode == self::WHITESPACE_IGNORE_ALL));
$force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL);
if (!$force_ignore) {
if ($ignore_all && $changeset->getWhitespaceMatters()) {
$ignore_all = false;
}
}
// The "ignore all whitespace" algorithm depends on rediffing the
// files, and we currently need complete representations of both
// files to do anything reasonable. If we only have parts of the files,
// don't use the "ignore all" algorithm.
if ($ignore_all) {
$hunks = $changeset->getHunks();
if (count($hunks) !== 1) {
$ignore_all = false;
} else {
$first_hunk = reset($hunks);
if ($first_hunk->getOldOffset() != 1 ||
$first_hunk->getNewOffset() != 1) {
$ignore_all = false;
}
}
}
if ($ignore_all) {
$old_file = $changeset->makeOldFile();
$new_file = $changeset->makeNewFile();
if ($old_file == $new_file) {
// If the old and new files are exactly identical, the synthetic
// diff below will give us nonsense and whitespace modes are
// irrelevant anyway. This occurs when you, e.g., copy a file onto
// itself in Subversion (see T271).
$ignore_all = false;
}
}
$hunk_parser = new DifferentialHunkParser();
$hunk_parser->setWhitespaceMode($whitespace_mode);
$hunk_parser->parseHunksForLineData($changeset->getHunks());
// Depending on the whitespace mode, we may need to compute a different
// set of changes than the set of changes in the hunk data (specifically,
// we might want to consider changed lines which have only whitespace
// changes as unchanged).
if ($ignore_all) {
$engine = new PhabricatorDifferenceEngine();
$engine->setIgnoreWhitespace(true);
$no_whitespace_changeset = $engine->generateChangesetFromFileContent(
$old_file,
$new_file);
$type_parser = new DifferentialHunkParser();
$type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks());
$hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
$hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
}
$hunk_parser->reparseHunksForSpecialAttributes();
$unchanged = false;
if (!$hunk_parser->getHasAnyChanges()) {
$filetype = $this->changeset->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
$unchanged = true;
}
}
$moveaway = false;
$changetype = $this->changeset->getChangeType();
if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
$moveaway = true;
}
$this->setSpecialAttributes(array(
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => $hunk_parser->getIsDeleted(),
self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(),
self::ATTR_MOVEAWAY => $moveaway,
));
$lines_context = $this->getLinesOfContext();
$hunk_parser->generateIntraLineDiffs();
$hunk_parser->generateVisibileLinesMask($lines_context);
$this->setOldLines($hunk_parser->getOldLines());
$this->setNewLines($hunk_parser->getNewLines());
$this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
$this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
$this->hunkStartLines = $hunk_parser->getHunkStartLines(
$changeset->getHunks());
$new_corpus = $hunk_parser->getNewCorpus();
$new_corpus_block = implode('', $new_corpus);
$this->markGenerated($new_corpus_block);
if ($this->isTopLevel &&
!$this->comments &&
($this->isGenerated() ||
$this->isUnchanged() ||
$this->isDeleted())) {
return;
}
$old_corpus = $hunk_parser->getOldCorpus();
$old_corpus_block = implode('', $old_corpus);
$old_future = $this->getHighlightFuture($old_corpus_block);
$new_future = $this->getHighlightFuture($new_corpus_block);
$futures = array(
'old' => $old_future,
'new' => $new_future,
);
$corpus_blocks = array(
'old' => $old_corpus_block,
'new' => $new_corpus_block,
);
$this->highlightErrors = false;
foreach (new FutureIterator($futures) as $key => $future) {
try {
try {
$highlighted = $future->resolve();
} catch (PhutilSyntaxHighlighterException $ex) {
$this->highlightErrors = true;
$highlighted = id(new PhutilDefaultSyntaxHighlighter())
->getHighlightFuture($corpus_blocks[$key])
->resolve();
}
switch ($key) {
case 'old':
$this->oldRender = $this->processHighlightedSource(
$this->old,
$highlighted);
break;
case 'new':
$this->newRender = $this->processHighlightedSource(
$this->new,
$highlighted);
break;
}
} catch (Exception $ex) {
phlog($ex);
throw $ex;
}
}
$this->applyIntraline(
$this->oldRender,
ipull($this->intra, 0),
$old_corpus);
$this->applyIntraline(
$this->newRender,
ipull($this->intra, 1),
$new_corpus);
}
private function shouldRenderPropertyChangeHeader($changeset) {
if (!$this->isTopLevel) {
// We render properties only at top level; otherwise we get multiple
// copies of them when a user clicks "Show More".
return false;
}
return true;
}
public function render(
$range_start = null,
$range_len = null,
$mask_force = array()) {
// "Top level" renders are initial requests for the whole file, versus
// requests for a specific range generated by clicking "show more". We
// generate property changes and "shield" UI elements only for toplevel
// requests.
$this->isTopLevel = (($range_start === null) && ($range_len === null));
$this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
$encoding = null;
if ($this->characterEncoding) {
// We are forcing this changeset to be interpreted with a specific
// character encoding, so force all the hunks into that encoding and
// propagate it to the renderer.
$encoding = $this->characterEncoding;
foreach ($this->changeset->getHunks() as $hunk) {
$hunk->forceEncoding($this->characterEncoding);
}
} else {
// We're just using the default, so tell the renderer what that is
// (by reading the encoding from the first hunk).
foreach ($this->changeset->getHunks() as $hunk) {
$encoding = $hunk->getDataEncoding();
break;
}
}
$this->tryCacheStuff();
// If we're rendering in an offset mode, treat the range numbers as line
// numbers instead of rendering offsets.
$offset_mode = $this->getOffsetMode();
if ($offset_mode) {
if ($offset_mode == 'new') {
$offset_map = $this->new;
} else {
$offset_map = $this->old;
}
$range_end = $this->getOffset($offset_map, $range_start + $range_len);
$range_start = $this->getOffset($offset_map, $range_start);
$range_len = ($range_end - $range_start);
}
$render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);
$rows = max(
count($this->old),
count($this->new));
$renderer = $this->getRenderer()
->setUser($this->getUser())
->setChangeset($this->changeset)
->setRenderPropertyChangeHeader($render_pch)
->setIsTopLevel($this->isTopLevel)
->setOldRender($this->oldRender)
->setNewRender($this->newRender)
->setHunkStartLines($this->hunkStartLines)
->setOldChangesetID($this->leftSideChangesetID)
->setNewChangesetID($this->rightSideChangesetID)
->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)
->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)
->setCodeCoverage($this->getCoverage())
->setRenderingReference($this->getRenderingReference())
->setMarkupEngine($this->markupEngine)
->setHandles($this->handles)
->setOldLines($this->old)
->setNewLines($this->new)
->setOriginalCharacterEncoding($encoding)
->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks())
->setCanMarkDone($this->getCanMarkDone())
->setObjectOwnerPHID($this->getObjectOwnerPHID())
->setHighlightingDisabled($this->highlightingDisabled);
$shield = null;
if ($this->isTopLevel && !$this->comments) {
if ($this->isGenerated()) {
$shield = $renderer->renderShield(
pht(
'This file contains generated code, which does not normally '.
'need to be reviewed.'));
} else if ($this->isMoveAway()) {
// We put an empty shield on these files. Normally, they do not have
// any diff content anyway. However, if they come through `arc`, they
// may have content. We don't want to show it (it's not useful) and
// we bailed out of fully processing it earlier anyway.
// We could show a message like "this file was moved", but we show
// that as a change header anyway, so it would be redundant. Instead,
// just render an empty shield to skip rendering the diff body.
$shield = '';
} else if ($this->isUnchanged()) {
$type = 'text';
if (!$rows) {
// NOTE: Normally, diffs which don't change files do not include
// file content (for example, if you "chmod +x" a file and then
// run "git show", the file content is not available). Similarly,
// if you move a file from A to B without changing it, diffs normally
// do not show the file content. In some cases `arc` is able to
// synthetically generate content for these diffs, but for raw diffs
// we'll never have it so we need to be prepared to not render a link.
$type = 'none';
}
$type_add = DifferentialChangeType::TYPE_ADD;
if ($this->changeset->getChangeType() == $type_add) {
// Although the generic message is sort of accurate in a technical
// sense, this more-tailored message is less confusing.
$shield = $renderer->renderShield(
pht('This is an empty file.'),
$type);
} else {
$shield = $renderer->renderShield(
pht('The contents of this file were not changed.'),
$type);
}
} else if ($this->isWhitespaceOnly()) {
$shield = $renderer->renderShield(
pht('This file was changed only by adding or removing whitespace.'),
'whitespace');
} else if ($this->isDeleted()) {
$shield = $renderer->renderShield(
pht('This file was completely deleted.'));
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$shield = $renderer->renderShield(
pht(
'This file has a very large number of changes (%s lines).',
new PhutilNumber($this->changeset->getAffectedLineCount())));
}
}
if ($shield !== null) {
return $renderer->renderChangesetTable($shield);
}
// This request should render the "undershield" headers if it's a top-level
// request which made it this far (indicating the changeset has no shield)
// or it's a request with no mask information (indicating it's the request
// that removes the rendering shield). Possibly, this second class of
// request might need to be made more explicit.
$is_undershield = (empty($mask_force) || $this->isTopLevel);
$renderer->setIsUndershield($is_undershield);
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
$lines_context = $this->getLinesOfContext();
if ($this->comments) {
// If there are any comments which appear in sections of the file which
// we don't have, we're going to move them backwards to the closest
// earlier line. Two cases where this may happen are:
//
// - Porting ghost comments forward into a file which was mostly
// deleted.
// - Porting ghost comments forward from a full-context diff to a
// partial-context diff.
list($old_backmap, $new_backmap) = $this->buildLineBackmaps();
foreach ($this->comments as $comment) {
$new_side = $this->isCommentOnRightSideWhenDisplayed($comment);
$line = $comment->getLineNumber();
if ($new_side) {
$back_line = $new_backmap[$line];
} else {
$back_line = $old_backmap[$line];
}
if ($back_line != $line) {
// TODO: This should probably be cleaner, but just be simple and
// obvious for now.
$ghost = $comment->getIsGhost();
if ($ghost) {
$moved = pht(
'This comment originally appeared on line %s, but that line '.
'does not exist in this version of the diff. It has been '.
'moved backward to the nearest line.',
new PhutilNumber($line));
$ghost['reason'] = $ghost['reason']."\n\n".$moved;
$comment->setIsGhost($ghost);
}
$comment->setLineNumber($back_line);
$comment->setLineLength(0);
}
$start = max($comment->getLineNumber() - $lines_context, 0);
$end = $comment->getLineNumber() +
$comment->getLineLength() +
$lines_context;
for ($ii = $start; $ii <= $end; $ii++) {
if ($new_side) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
$this->comments = id(new PHUIDiffInlineThreader())
->reorderAndThreadCommments($this->comments);
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
$comment->getLineLength();
$final = max(1, $final);
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[$final][] = $comment;
} else {
$old_comments[$final][] = $comment;
}
}
}
$renderer
->setOldComments($old_comments)
->setNewComments($new_comments);
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$new = null;
// TODO: Improve the architectural issue as discussed in D955
// https://secure.phabricator.com/D955
$reference = $this->getRenderingReference();
$parts = explode('/', $reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
if (!$vs) {
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
} else {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
$old_phid = null;
$new_phid = null;
// TODO: This is spooky, see D6851
if ($vs_changeset) {
$vs_metadata = $vs_changeset->getMetadata();
$old_phid = idx($vs_metadata, 'new:binary-phid');
}
$changeset = id(new DifferentialChangeset())->load($id);
if ($changeset) {
$metadata = $changeset->getMetadata();
$new_phid = idx($metadata, 'new:binary-phid');
}
}
if ($old_phid || $new_phid) {
// grab the files, (micro) optimization for 1 query not 2
$file_phids = array();
if ($old_phid) {
$file_phids[] = $old_phid;
}
if ($new_phid) {
$file_phids[] = $new_phid;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getUser())
->withPHIDs($file_phids)
->execute();
foreach ($files as $file) {
if (empty($file)) {
continue;
}
if ($file->getPHID() == $old_phid) {
$old = $file;
} else if ($file->getPHID() == $new_phid) {
$new = $file;
}
}
}
$renderer->attachOldFile($old);
$renderer->attachNewFile($new);
return $renderer->renderFileChange($old, $new, $id, $vs);
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $renderer->renderChangesetTable(null);
return $output;
}
if ($this->originalLeft && $this->originalRight) {
list($highlight_old, $highlight_new) = $this->diffOriginals();
$highlight_old = array_flip($highlight_old);
$highlight_new = array_flip($highlight_new);
$renderer
->setHighlightOld($highlight_old)
->setHighlightNew($highlight_new);
}
$renderer
->setOriginalOld($this->originalLeft)
->setOriginalNew($this->originalRight);
if ($range_start === null) {
$range_start = 0;
}
if ($range_len === null) {
$range_len = $rows;
}
$range_len = min($range_len, $rows - $range_start);
list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
$mask_force,
$feedback_mask,
$range_start,
$range_len);
$renderer
->setGaps($gaps)
->setMask($mask)
->setDepths($depths);
$html = $renderer->renderTextChange(
$range_start,
$range_len,
$rows);
return $renderer->renderChangesetTable($html);
}
/**
* This function calculates a lot of stuff we need to know to display
* the diff:
*
* Gaps - compute gaps in the visible display diff, where we will render
* "Show more context" spacers. If a gap is smaller than the context size,
* we just display it. Otherwise, we record it into $gaps and will render a
* "show more context" element instead of diff text below. A given $gap
* is a tuple of $gap_line_number_start and $gap_length.
*
* Mask - compute the actual lines that need to be shown (because they
* are near changes lines, near inline comments, or the request has
* explicitly asked for them, i.e. resulting from the user clicking
* "show more"). The $mask returned is a sparsely populated dictionary
* of $visible_line_number => true.
*
* Depths - compute how indented any given line is. The $depths returned
* is a sparsely populated dictionary of $visible_line_number => $depth.
*
* This function also has the side effect of modifying member variable
* new such that tabs are normalized to spaces for each line of the diff.
*
* @return array($gaps, $mask, $depths)
*/
private function calculateGapsMaskAndDepths(
$mask_force,
$feedback_mask,
$range_start,
$range_len) {
$lines_context = $this->getLinesOfContext();
// Calculate gaps and mask first
$gaps = array();
$gap_start = 0;
$in_gap = false;
$base_mask = $this->visible + $mask_force + $feedback_mask;
$base_mask[$range_start + $range_len] = true;
for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
if (isset($base_mask[$ii])) {
if ($in_gap) {
$gap_length = $ii - $gap_start;
if ($gap_length <= $lines_context) {
for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
$base_mask[$jj] = true;
}
} else {
$gaps[] = array($gap_start, $gap_length);
}
$in_gap = false;
}
} else {
if (!$in_gap) {
$gap_start = $ii;
$in_gap = true;
}
}
}
$gaps = array_reverse($gaps);
$mask = $base_mask;
// Time to calculate depth.
// We need to go backwards to properly indent whitespace in this code:
//
// 0: class C {
// 1:
// 1: function f() {
// 2:
// 2: return;
// 1:
// 1: }
// 0:
// 0: }
//
$depths = array();
$last_depth = 0;
$range_end = $range_start + $range_len;
if (!isset($this->new[$range_end])) {
$range_end--;
}
for ($ii = $range_end; $ii >= $range_start; $ii--) {
// We need to expand tabs to process mixed indenting and to round
// correctly later.
$line = str_replace("\t", ' ', $this->new[$ii]['text']);
$trimmed = ltrim($line);
if ($trimmed != '') {
// We round down to flatten "/**" and " *".
$last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
}
$depths[$ii] = $last_depth;
}
return array($gaps, $mask, $depths);
}
/**
* Determine if an inline comment will appear on the rendered diff,
* taking into consideration which halves of which changesets will actually
* be shown.
*
* @param PhabricatorInlineCommentInterface Comment to test for visibility.
* @return bool True if the comment is visible on the rendered diff.
*/
private function isCommentVisibleOnRenderedDiff(
PhabricatorInlineCommentInterface $comment) {
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Determine if a comment will appear on the right side of the display diff.
* Note that the comment must appear somewhere on the rendered changeset, as
* per isCommentVisibleOnRenderedDiff().
*
* @param PhabricatorInlineCommentInterface Comment to test for display
* location.
* @return bool True for right, false for left.
*/
private function isCommentOnRightSideWhenDisplayed(
PhabricatorInlineCommentInterface $comment) {
if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
throw new Exception(pht('Comment is not visible on changeset!'));
}
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Parse the 'range' specification that this class and the client-side JS
* emit to indicate that a user clicked "Show more..." on a diff. Generally,
* use is something like this:
*
* $spec = $request->getStr('range');
* $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
* list($start, $end, $mask) = $parsed;
* $parser->render($start, $end, $mask);
*
* @param string Range specification, indicating the range of the diff that
* should be rendered.
* @return tuple List of <start, end, mask> suitable for passing to
* @{method:render}.
*/
public static function parseRangeSpecification($spec) {
$range_s = null;
$range_e = null;
$mask = array();
if ($spec) {
$match = null;
if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
$range_s = (int)$match[1];
$range_e = (int)$match[2];
if (count($match) > 3) {
$start = (int)$match[3];
$len = (int)$match[4];
for ($ii = $start; $ii < $start + $len; $ii++) {
$mask[$ii] = true;
}
}
}
}
return array($range_s, $range_e, $mask);
}
/**
* Render "modified coverage" information; test coverage on modified lines.
* This synthesizes diff information with unit test information into a useful
* indicator of how well tested a change is.
*/
public function renderModifiedCoverage() {
$na = phutil_tag('em', array(), '-');
$coverage = $this->getCoverage();
if (!$coverage) {
return $na;
}
$covered = 0;
$not_covered = 0;
foreach ($this->new as $k => $new) {
if (!$new['line']) {
continue;
}
if (!$new['type']) {
continue;
}
if (empty($coverage[$new['line'] - 1])) {
continue;
}
switch ($coverage[$new['line'] - 1]) {
case 'C':
$covered++;
break;
case 'U':
$not_covered++;
break;
}
}
if (!$covered && !$not_covered) {
return $na;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
- public function detectCopiedCode(
- array $changesets,
- $min_width = 30,
- $min_lines = 3) {
-
- assert_instances_of($changesets, 'DifferentialChangeset');
-
- $map = array();
- $files = array();
- $types = array();
- foreach ($changesets as $changeset) {
- $file = $changeset->getFilename();
- foreach ($changeset->getHunks() as $hunk) {
- $lines = $hunk->getStructuredOldFile();
- foreach ($lines as $line => $info) {
- $type = $info['type'];
- if ($type == '\\') {
- continue;
- }
- $types[$file][$line] = $type;
-
- $text = $info['text'];
- $text = trim($text);
- $files[$file][$line] = $text;
-
- if (strlen($text) >= $min_width) {
- $map[$text][] = array($file, $line);
- }
- }
- }
- }
-
- foreach ($changesets as $changeset) {
- $copies = array();
- foreach ($changeset->getHunks() as $hunk) {
- $added = $hunk->getStructuredNewFile();
- $atype = array();
-
- foreach ($added as $line => $info) {
- $atype[$line] = $info['type'];
- $added[$line] = trim($info['text']);
- }
-
- $skip_lines = 0;
- foreach ($added as $line => $code) {
- if ($skip_lines) {
- // We're skipping lines that we already processed because we
- // extended a block above them downward to include them.
- $skip_lines--;
- continue;
- }
-
- if ($atype[$line] !== '+') {
- // This line hasn't been changed in the new file, so don't try
- // to figure out where it came from.
- continue;
- }
-
- if (empty($map[$code])) {
- // This line was too short to trigger copy/move detection.
- continue;
- }
-
- if (count($map[$code]) > 16) {
- // If there are a large number of identical lines in this diff,
- // don't try to figure out where this block came from: the analysis
- // is O(N^2), since we need to compare every line against every
- // other line. Even if we arrive at a result, it is unlikely to be
- // meaningful. See T5041.
- continue;
- }
-
- $best_length = 0;
-
- // Explore all candidates.
- foreach ($map[$code] as $val) {
- list($file, $orig_line) = $val;
- $length = 1;
-
- // Search backward and forward to find all of the adjacent lines
- // which match.
- foreach (array(-1, 1) as $direction) {
- $offset = $direction;
- while (true) {
- if (isset($copies[$line + $offset])) {
- // If we run into a block above us which we've already
- // attributed to a move or copy from elsewhere, stop
- // looking.
- break;
- }
-
- if (!isset($added[$line + $offset])) {
- // If we've run off the beginning or end of the new file,
- // stop looking.
- break;
- }
-
- if (!isset($files[$file][$orig_line + $offset])) {
- // If we've run off the beginning or end of the original
- // file, we also stop looking.
- break;
- }
-
- $old = $files[$file][$orig_line + $offset];
- $new = $added[$line + $offset];
- if ($old !== $new) {
- // If the old line doesn't match the new line, stop
- // looking.
- break;
- }
-
- $length++;
- $offset += $direction;
- }
- }
-
- if ($length < $best_length) {
- // If we already know of a better source (more matching lines)
- // for this move/copy, stick with that one. We prefer long
- // copies/moves which match a lot of context over short ones.
- continue;
- }
-
- if ($length == $best_length) {
- if (idx($types[$file], $orig_line) != '-') {
- // If we already know of an equally good source (same number
- // of matching lines) and this isn't a move, stick with the
- // other one. We prefer moves over copies.
- continue;
- }
- }
-
- $best_length = $length;
- // ($offset - 1) contains number of forward matching lines.
- $best_offset = $offset - 1;
- $best_file = $file;
- $best_line = $orig_line;
- }
-
- $file = ($best_file == $changeset->getFilename() ? '' : $best_file);
- for ($i = $best_length; $i--; ) {
- $type = idx($types[$best_file], $best_line + $best_offset - $i);
- $copies[$line + $best_offset - $i] = ($best_length < $min_lines
- ? array() // Ignore short blocks.
- : array($file, $best_line + $best_offset - $i, $type));
- }
-
- $skip_lines = $best_offset;
- }
- }
-
- $copies = array_filter($copies);
- if ($copies) {
- $metadata = $changeset->getMetadata();
- $metadata['copy:lines'] = $copies;
- $changeset->setMetadata($metadata);
- }
- }
- return $changesets;
- }
-
/**
* Build maps from lines comments appear on to actual lines.
*/
private function buildLineBackmaps() {
$old_back = array();
$new_back = array();
foreach ($this->old as $ii => $old) {
$old_back[$old['line']] = $old['line'];
}
foreach ($this->new as $ii => $new) {
$new_back[$new['line']] = $new['line'];
}
$max_old_line = 0;
$max_new_line = 0;
foreach ($this->comments as $comment) {
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$max_new_line = max($max_new_line, $comment->getLineNumber());
} else {
$max_old_line = max($max_old_line, $comment->getLineNumber());
}
}
$cursor = 1;
for ($ii = 1; $ii <= $max_old_line; $ii++) {
if (empty($old_back[$ii])) {
$old_back[$ii] = $cursor;
} else {
$cursor = $old_back[$ii];
}
}
$cursor = 1;
for ($ii = 1; $ii <= $max_new_line; $ii++) {
if (empty($new_back[$ii])) {
$new_back[$ii] = $cursor;
} else {
$cursor = $new_back[$ii];
}
}
return array($old_back, $new_back);
}
private function getOffset(array $map, $line) {
if (!$map) {
return null;
}
$line = (int)$line;
foreach ($map as $key => $spec) {
if ($spec && isset($spec['line'])) {
if ((int)$spec['line'] >= $line) {
return $key;
}
}
}
return $key;
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
index 586680d1b..90c397790 100644
--- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
@@ -1,271 +1,279 @@
<?php
final class DifferentialChangesetOneUpRenderer
extends DifferentialChangesetHTMLRenderer {
public function isOneUpRenderer() {
return true;
}
protected function getRendererTableClass() {
return 'diff-1up';
}
public function getRendererKey() {
return '1up';
}
protected function renderColgroup() {
return phutil_tag('colgroup', array(), array(
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'copy')),
phutil_tag('col', array('class' => 'unified')),
));
}
public function renderTextChange(
$range_start,
$range_len,
$rows) {
$primitives = $this->buildPrimitives($range_start, $range_len);
return $this->renderPrimitives($primitives, $rows);
}
protected function renderPrimitives(array $primitives, $rows) {
list($left_prefix, $right_prefix) = $this->getLineIDPrefixes();
$no_copy = phutil_tag('td', array('class' => 'copy'));
$no_coverage = null;
$column_width = 4;
$aural_minus = javelin_tag(
'span',
array(
'aural' => true,
),
'- ');
$aural_plus = javelin_tag(
'span',
array(
'aural' => true,
),
'+ ');
$out = array();
foreach ($primitives as $k => $p) {
$type = $p['type'];
switch ($type) {
case 'old':
case 'new':
case 'old-file':
case 'new-file':
$is_old = ($type == 'old' || $type == 'old-file');
$cells = array();
if ($is_old) {
if ($p['htype']) {
- $class = 'left old';
+ if (empty($p['oline'])) {
+ $class = 'left old old-full';
+ } else {
+ $class = 'left old';
+ }
$aural = $aural_minus;
} else {
$class = 'left';
$aural = null;
}
if ($type == 'old-file') {
$class = "{$class} differential-old-image";
}
if ($left_prefix) {
$left_id = $left_prefix.$p['line'];
} else {
$left_id = null;
}
$line = $p['line'];
$cells[] = phutil_tag(
'th',
array(
'id' => $left_id,
'class' => $class,
),
$line);
$render = $p['render'];
if ($aural !== null) {
$render = array($aural, $render);
}
$cells[] = phutil_tag('th', array('class' => $class));
$cells[] = $no_copy;
$cells[] = phutil_tag('td', array('class' => $class), $render);
$cells[] = $no_coverage;
} else {
if ($p['htype']) {
- $class = 'right new';
+ if (empty($p['oline'])) {
+ $class = 'right new new-full';
+ } else {
+ $class = 'right new';
+ }
$cells[] = phutil_tag('th', array('class' => $class));
$aural = $aural_plus;
} else {
$class = 'right';
if ($left_prefix) {
$left_id = $left_prefix.$p['oline'];
} else {
$left_id = null;
}
$oline = $p['oline'];
$cells[] = phutil_tag('th', array('id' => $left_id), $oline);
$aural = null;
}
if ($type == 'new-file') {
$class = "{$class} differential-new-image";
}
if ($right_prefix) {
$right_id = $right_prefix.$p['line'];
} else {
$right_id = null;
}
$line = $p['line'];
$cells[] = phutil_tag(
'th',
array(
'id' => $right_id,
'class' => $class,
),
$line);
$render = $p['render'];
if ($aural !== null) {
$render = array($aural, $render);
}
$cells[] = $no_copy;
$cells[] = phutil_tag('td', array('class' => $class), $render);
$cells[] = $no_coverage;
}
$out[] = phutil_tag('tr', array(), $cells);
break;
case 'inline':
$inline = $this->buildInlineComment(
$p['comment'],
$p['right']);
$out[] = $this->getRowScaffoldForInline($inline);
break;
case 'no-context':
$out[] = phutil_tag(
'tr',
array(),
phutil_tag(
'td',
array(
'class' => 'show-more',
'colspan' => $column_width,
),
pht('Context not available.')));
break;
case 'context':
$top = $p['top'];
$len = $p['len'];
$links = $this->renderShowContextLinks($top, $len, $rows);
$out[] = javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_tag(
'td',
array(
'class' => 'show-more',
'colspan' => $column_width,
),
$links));
break;
default:
$out[] = hsprintf('<tr><th /><th /><td>%s</td></tr>', $type);
break;
}
}
if ($out) {
return $this->wrapChangeInTable(phutil_implode_html('', $out));
}
return null;
}
public function renderFileChange(
$old_file = null,
$new_file = null,
$id = 0,
$vs = 0) {
// TODO: This should eventually merge into the normal primitives pathway,
// but fake it for now and just share as much code as possible.
$primitives = array();
if ($old_file) {
$primitives[] = array(
'type' => 'old-file',
'htype' => ($new_file ? 'new-file' : null),
'file' => $old_file,
'line' => 1,
'render' => $this->renderImageStage($old_file),
);
}
if ($new_file) {
$primitives[] = array(
'type' => 'new-file',
'htype' => ($old_file ? 'old-file' : null),
'file' => $new_file,
'line' => 1,
'oline' => ($old_file ? 1 : null),
'render' => $this->renderImageStage($new_file),
);
}
// TODO: We'd like to share primitive code here, but buildPrimitives()
// currently chokes on changesets with no textual data.
foreach ($this->getOldComments() as $line => $group) {
foreach ($group as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => false,
);
}
}
foreach ($this->getNewComments() as $line => $group) {
foreach ($group as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => true,
);
}
}
$output = $this->renderPrimitives($primitives, 1);
return $this->renderChangesetTable($output);
}
public function getRowScaffoldForInline(PHUIDiffInlineCommentView $view) {
return id(new PHUIDiffOneUpInlineCommentRowScaffold())
->addInlineView($view);
}
}
diff --git a/src/applications/differential/render/DifferentialRawDiffRenderer.php b/src/applications/differential/render/DifferentialRawDiffRenderer.php
index 0b826e9ba..4f25646e4 100644
--- a/src/applications/differential/render/DifferentialRawDiffRenderer.php
+++ b/src/applications/differential/render/DifferentialRawDiffRenderer.php
@@ -1,66 +1,79 @@
<?php
final class DifferentialRawDiffRenderer extends Phobject {
private $changesets;
private $format = 'unified';
private $viewer;
+ private $byteLimit;
public function setFormat($format) {
$this->format = $format;
return $this;
}
public function getFormat() {
return $this->format;
}
public function setChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->changesets;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
+ public function setByteLimit($byte_limit) {
+ $this->byteLimit = $byte_limit;
+ return $this;
+ }
+
+ public function getByteLimit() {
+ return $this->byteLimit;
+ }
+
public function buildPatch() {
$diff = new DifferentialDiff();
$diff->attachChangesets($this->getChangesets());
$raw_changes = $diff->buildChangesList();
$changes = array();
foreach ($raw_changes as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$viewer = $this->getViewer();
$loader = id(new PhabricatorFileBundleLoader())
->setViewer($viewer);
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
+ $byte_limit = $this->getByteLimit();
+ if ($byte_limit) {
+ $bundle->setByteLimit($byte_limit);
+ }
+
$format = $this->getFormat();
switch ($format) {
case 'git':
return $bundle->toGitPatch();
- break;
case 'unified':
default:
return $bundle->toUnifiedDiff();
- break;
}
}
}
diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php
index 4832f452e..00af84bc4 100644
--- a/src/applications/differential/storage/DifferentialChangeset.php
+++ b/src/applications/differential/storage/DifferentialChangeset.php
@@ -1,308 +1,440 @@
<?php
final class DifferentialChangeset
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $diffID;
protected $oldFile;
protected $filename;
protected $awayPaths;
protected $changeType;
protected $fileType;
- protected $metadata;
+ protected $metadata = array();
protected $oldProperties;
protected $newProperties;
protected $addLines;
protected $delLines;
private $unsavedHunks = array();
private $hunks = self::ATTACHABLE;
private $diff = self::ATTACHABLE;
const TABLE_CACHE = 'differential_changeset_parse_cache';
+ const METADATA_TRUSTED_ATTRIBUTES = 'attributes.trusted';
+ const METADATA_UNTRUSTED_ATTRIBUTES = 'attributes.untrusted';
+ const METADATA_EFFECT_HASH = 'hash.effect';
+
+ const ATTRIBUTE_GENERATED = 'generated';
+
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
'oldProperties' => self::SERIALIZATION_JSON,
'newProperties' => self::SERIALIZATION_JSON,
'awayPaths' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'oldFile' => 'bytes?',
'filename' => 'bytes',
'changeType' => 'uint32',
'fileType' => 'uint32',
'addLines' => 'uint32',
'delLines' => 'uint32',
// T6203/NULLABILITY
// These should all be non-nullable, and store reasonable default
// JSON values if empty.
'awayPaths' => 'text?',
'metadata' => 'text?',
'oldProperties' => 'text?',
'newProperties' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'diffID' => array(
'columns' => array('diffID'),
),
),
) + parent::getConfiguration();
}
public function getAffectedLineCount() {
return $this->getAddLines() + $this->getDelLines();
}
public function attachHunks(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$this->hunks = $hunks;
return $this;
}
public function getHunks() {
return $this->assertAttached($this->hunks);
}
public function getDisplayFilename() {
$name = $this->getFilename();
if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$name .= '/';
}
return $name;
}
public function getOwnersFilename() {
// TODO: For Subversion, we should adjust these paths to be relative to
// the repository root where possible.
$path = $this->getFilename();
if (!isset($path[0])) {
return '/';
}
if ($path[0] != '/') {
$path = '/'.$path;
}
return $path;
}
public function addUnsavedHunk(DifferentialHunk $hunk) {
if ($this->hunks === self::ATTACHABLE) {
$this->hunks = array();
}
$this->hunks[] = $hunk;
$this->unsavedHunks[] = $hunk;
return $this;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedHunks as $hunk) {
$hunk->setChangesetID($this->getID());
$hunk->save();
}
$this->saveTransaction();
return $ret;
}
public function delete() {
$this->openTransaction();
$hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($hunks as $hunk) {
$hunk->delete();
}
$this->unsavedHunks = array();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE id = %d',
self::TABLE_CACHE,
$this->getID());
$ret = parent::delete();
$this->saveTransaction();
return $ret;
}
+ /**
+ * Test if this changeset and some other changeset put the affected file in
+ * the same state.
+ *
+ * @param DifferentialChangeset Changeset to compare against.
+ * @return bool True if the two changesets have the same effect.
+ */
+ public function hasSameEffectAs(DifferentialChangeset $other) {
+ if ($this->getFilename() !== $other->getFilename()) {
+ return false;
+ }
+
+ $hash_key = self::METADATA_EFFECT_HASH;
+
+ $u_hash = $this->getChangesetMetadata($hash_key);
+ if ($u_hash === null) {
+ return false;
+ }
+
+ $v_hash = $other->getChangesetMetadata($hash_key);
+ if ($v_hash === null) {
+ return false;
+ }
+
+ if ($u_hash !== $v_hash) {
+ return false;
+ }
+
+ // Make sure the final states for the file properties (like the "+x"
+ // executable bit) match one another.
+ $u_props = $this->getNewProperties();
+ $v_props = $other->getNewProperties();
+ ksort($u_props);
+ ksort($v_props);
+
+ if ($u_props !== $v_props) {
+ return false;
+ }
+
+ return true;
+ }
+
public function getSortKey() {
$sort_key = $this->getFilename();
// Sort files with ".h" in them first, so headers (.h, .hpp) come before
// implementations (.c, .cpp, .cs).
$sort_key = str_replace('.h', '.!h', $sort_key);
return $sort_key;
}
public function makeNewFile() {
$file = mpull($this->getHunks(), 'makeNewFile');
return implode('', $file);
}
public function makeOldFile() {
$file = mpull($this->getHunks(), 'makeOldFile');
return implode('', $file);
}
public function makeChangesWithContext($num_lines = 3) {
$with_context = array();
foreach ($this->getHunks() as $hunk) {
$context = array();
$changes = explode("\n", $hunk->getChanges());
foreach ($changes as $l => $line) {
$type = substr($line, 0, 1);
if ($type == '+' || $type == '-') {
$context += array_fill($l - $num_lines, 2 * $num_lines + 1, true);
}
}
$with_context[] = array_intersect_key($changes, $context);
}
return array_mergev($with_context);
}
public function getAnchorName() {
return 'change-'.PhabricatorHash::digestForAnchor($this->getFilename());
}
public function getAbsoluteRepositoryPath(
PhabricatorRepository $repository = null,
DifferentialDiff $diff = null) {
$base = '/';
if ($diff && $diff->getSourceControlPath()) {
$base = id(new PhutilURI($diff->getSourceControlPath()))->getPath();
}
$path = $this->getFilename();
$path = rtrim($base, '/').'/'.ltrim($path, '/');
$svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
if ($repository && $repository->getVersionControlSystem() == $svn) {
$prefix = $repository->getDetail('remote-uri');
$prefix = id(new PhutilURI($prefix))->getPath();
if (!strncmp($path, $prefix, strlen($prefix))) {
$path = substr($path, strlen($prefix));
}
$path = '/'.ltrim($path, '/');
}
return $path;
}
public function getWhitespaceMatters() {
$config = PhabricatorEnv::getEnvConfig('differential.whitespace-matters');
foreach ($config as $regexp) {
if (preg_match($regexp, $this->getFilename())) {
return true;
}
}
return false;
}
public function attachDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function getDiff() {
return $this->assertAttached($this->diff);
}
public function newFileTreeIcon() {
$file_type = $this->getFileType();
$change_type = $this->getChangeType();
$change_icons = array(
DifferentialChangeType::TYPE_DELETE => 'fa-file-o',
);
if (isset($change_icons[$change_type])) {
$icon = $change_icons[$change_type];
} else {
$icon = DifferentialChangeType::getIconForFileType($file_type);
}
$change_colors = array(
DifferentialChangeType::TYPE_ADD => 'green',
DifferentialChangeType::TYPE_DELETE => 'red',
DifferentialChangeType::TYPE_MOVE_AWAY => 'orange',
DifferentialChangeType::TYPE_MOVE_HERE => 'orange',
DifferentialChangeType::TYPE_COPY_HERE => 'orange',
DifferentialChangeType::TYPE_MULTICOPY => 'orange',
);
$color = idx($change_colors, $change_type, 'bluetext');
return id(new PHUIIconView())
->setIcon($icon.' '.$color);
}
public function getFileTreeClass() {
switch ($this->getChangeType()) {
case DifferentialChangeType::TYPE_ADD:
return 'filetree-added';
case DifferentialChangeType::TYPE_DELETE:
return 'filetree-deleted';
case DifferentialChangeType::TYPE_MOVE_AWAY:
case DifferentialChangeType::TYPE_MOVE_HERE:
case DifferentialChangeType::TYPE_COPY_HERE:
case DifferentialChangeType::TYPE_MULTICOPY:
return 'filetree-movecopy';
}
return null;
}
+ public function setChangesetMetadata($key, $value) {
+ if (!is_array($this->metadata)) {
+ $this->metadata = array();
+ }
+
+ $this->metadata[$key] = $value;
+
+ return $this;
+ }
+
+ public function getChangesetMetadata($key, $default = null) {
+ if (!is_array($this->metadata)) {
+ return $default;
+ }
+
+ return idx($this->metadata, $key, $default);
+ }
+
+ private function setInternalChangesetAttribute($trusted, $key, $value) {
+ if ($trusted) {
+ $meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
+ } else {
+ $meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
+ }
+
+ $attributes = $this->getChangesetMetadata($meta_key, array());
+ $attributes[$key] = $value;
+ $this->setChangesetMetadata($meta_key, $attributes);
+
+ return $this;
+ }
+
+ private function getInternalChangesetAttributes($trusted) {
+ if ($trusted) {
+ $meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
+ } else {
+ $meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
+ }
+
+ return $this->getChangesetMetadata($meta_key, array());
+ }
+
+ public function setTrustedChangesetAttribute($key, $value) {
+ return $this->setInternalChangesetAttribute(true, $key, $value);
+ }
+
+ public function getTrustedChangesetAttributes() {
+ return $this->getInternalChangesetAttributes(true);
+ }
+
+ public function getTrustedChangesetAttribute($key, $default = null) {
+ $map = $this->getTrustedChangesetAttributes();
+ return idx($map, $key, $default);
+ }
+
+ public function setUntrustedChangesetAttribute($key, $value) {
+ return $this->setInternalChangesetAttribute(false, $key, $value);
+ }
+
+ public function getUntrustedChangesetAttributes() {
+ return $this->getInternalChangesetAttributes(false);
+ }
+
+ public function getUntrustedChangesetAttribute($key, $default = null) {
+ $map = $this->getUntrustedChangesetAttributes();
+ return idx($map, $key, $default);
+ }
+
+ public function getChangesetAttributes() {
+ // Prefer trusted values over untrusted values when both exist.
+ return
+ $this->getTrustedChangesetAttributes() +
+ $this->getUntrustedChangesetAttributes();
+ }
+
+ public function getChangesetAttribute($key, $default = null) {
+ $map = $this->getChangesetAttributes();
+ return idx($map, $key, $default);
+ }
+
+ public function isGeneratedChangeset() {
+ return $this->getChangesetAttribute(self::ATTRIBUTE_GENERATED);
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getDiff()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getDiff()->hasAutomaticCapability($capability, $viewer);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($hunks as $hunk) {
$engine->destroyObject($hunk);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
index 728ec4b53..5f229f39b 100644
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -1,822 +1,821 @@
<?php
final class DifferentialDiff
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
HarbormasterBuildkiteBuildableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $revisionID;
protected $authorPHID;
protected $repositoryPHID;
protected $commitPHID;
protected $sourceMachine;
protected $sourcePath;
protected $sourceControlSystem;
protected $sourceControlBaseRevision;
protected $sourceControlPath;
protected $lintStatus;
protected $unitStatus;
protected $lineCount;
protected $branch;
protected $bookmark;
protected $creationMethod;
protected $repositoryUUID;
protected $description;
protected $viewPolicy;
private $unsavedChangesets = array();
private $changesets = self::ATTACHABLE;
private $revision = self::ATTACHABLE;
private $properties = array();
private $buildable = self::ATTACHABLE;
private $unitMessages = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'revisionID' => 'id?',
'authorPHID' => 'phid?',
'repositoryPHID' => 'phid?',
'sourceMachine' => 'text255?',
'sourcePath' => 'text255?',
'sourceControlSystem' => 'text64?',
'sourceControlBaseRevision' => 'text255?',
'sourceControlPath' => 'text255?',
'lintStatus' => 'uint32',
'unitStatus' => 'uint32',
'lineCount' => 'uint32',
'branch' => 'text255?',
'bookmark' => 'text255?',
'repositoryUUID' => 'text64?',
'commitPHID' => 'phid?',
// T6203/NULLABILITY
// These should be non-null; all diffs should have a creation method
// and the description should just be empty.
'creationMethod' => 'text255?',
'description' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'revisionID' => array(
'columns' => array('revisionID'),
),
'key_commit' => array(
'columns' => array('commitPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialDiffPHIDType::TYPECONST);
}
public function addUnsavedChangeset(DifferentialChangeset $changeset) {
if ($this->changesets === null) {
$this->changesets = array();
}
$this->unsavedChangesets[] = $changeset;
$this->changesets[] = $changeset;
return $this;
}
public function attachChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->assertAttached($this->changesets);
}
public function loadChangesets() {
if (!$this->getID()) {
return array();
}
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($changesets as $changeset) {
$changeset->attachDiff($this);
}
return $changesets;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedChangesets as $changeset) {
$changeset->setDiffID($this->getID());
$changeset->save();
}
$this->saveTransaction();
return $ret;
}
public static function initializeNewDiff(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
$diff = id(new DifferentialDiff())
->setViewPolicy($view_policy);
return $diff;
}
public static function newFromRawChanges(
PhabricatorUser $actor,
array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = self::initializeNewDiff($actor);
return self::buildChangesetsFromRawChanges($diff, $changes);
}
public static function newEphemeralFromRawChanges(array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = id(new DifferentialDiff())->makeEphemeral();
return self::buildChangesetsFromRawChanges($diff, $changes);
}
private static function buildChangesetsFromRawChanges(
DifferentialDiff $diff,
array $changes) {
// There may not be any changes; initialize the changesets list so that
// we don't throw later when accessing it.
$diff->attachChangesets(array());
$lines = 0;
foreach ($changes as $change) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
// If a user pastes a diff into Differential which includes a commit
// message (e.g., they ran `git show` to generate it), discard that
// change when constructing a DifferentialDiff.
continue;
}
$changeset = new DifferentialChangeset();
$add_lines = 0;
$del_lines = 0;
$first_line = PHP_INT_MAX;
$hunks = $change->getHunks();
if ($hunks) {
foreach ($hunks as $hunk) {
$dhunk = new DifferentialHunk();
$dhunk->setOldOffset($hunk->getOldOffset());
$dhunk->setOldLen($hunk->getOldLength());
$dhunk->setNewOffset($hunk->getNewOffset());
$dhunk->setNewLen($hunk->getNewLength());
$dhunk->setChanges($hunk->getCorpus());
$changeset->addUnsavedHunk($dhunk);
$add_lines += $hunk->getAddLines();
$del_lines += $hunk->getDelLines();
$added_lines = $hunk->getChangedLines('new');
if ($added_lines) {
$first_line = min($first_line, head_key($added_lines));
}
}
$lines += $add_lines + $del_lines;
} else {
// This happens when you add empty files.
$changeset->attachHunks(array());
}
$metadata = $change->getAllMetadata();
if ($first_line != PHP_INT_MAX) {
$metadata['line:first'] = $first_line;
}
$changeset->setOldFile($change->getOldPath());
$changeset->setFilename($change->getCurrentPath());
$changeset->setChangeType($change->getType());
$changeset->setFileType($change->getFileType());
$changeset->setMetadata($metadata);
$changeset->setOldProperties($change->getOldProperties());
$changeset->setNewProperties($change->getNewProperties());
$changeset->setAwayPaths($change->getAwayPaths());
$changeset->setAddLines($add_lines);
$changeset->setDelLines($del_lines);
$diff->addUnsavedChangeset($changeset);
}
$diff->setLineCount($lines);
- $parser = new DifferentialChangesetParser();
- $changesets = $parser->detectCopiedCode(
- $diff->getChangesets(),
- $min_width = 30,
- $min_lines = 3);
- $diff->attachChangesets($changesets);
+ $changesets = $diff->getChangesets();
+
+ id(new DifferentialChangesetEngine())
+ ->rebuildChangesets($changesets);
return $diff;
}
-
public function getDiffDict() {
$dict = array(
'id' => $this->getID(),
'revisionID' => $this->getRevisionID(),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
'sourceControlPath' => $this->getSourceControlPath(),
'sourceControlSystem' => $this->getSourceControlSystem(),
'branch' => $this->getBranch(),
'bookmark' => $this->getBookmark(),
'creationMethod' => $this->getCreationMethod(),
'description' => $this->getDescription(),
'unitStatus' => $this->getUnitStatus(),
'lintStatus' => $this->getLintStatus(),
'changes' => array(),
);
$dict['changes'] = $this->buildChangesList();
return $dict + $this->getDiffAuthorshipDict();
}
public function getDiffAuthorshipDict() {
$dict = array('properties' => array());
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $property) {
$dict['properties'][$property->getName()] = $property->getData();
if ($property->getName() == 'local:commits') {
foreach ($property->getData() as $commit) {
$dict['authorName'] = $commit['author'];
$dict['authorEmail'] = idx($commit, 'authorEmail');
break;
}
}
}
return $dict;
}
public function buildChangesList() {
$changes = array();
foreach ($this->getChangesets() as $changeset) {
$hunks = array();
foreach ($changeset->getHunks() as $hunk) {
$hunks[] = array(
'oldOffset' => $hunk->getOldOffset(),
'newOffset' => $hunk->getNewOffset(),
'oldLength' => $hunk->getOldLen(),
'newLength' => $hunk->getNewLen(),
'addLines' => null,
'delLines' => null,
'isMissingOldNewline' => null,
'isMissingNewNewline' => null,
'corpus' => $hunk->getChanges(),
);
}
$change = array(
'id' => $changeset->getID(),
'metadata' => $changeset->getMetadata(),
'oldPath' => $changeset->getOldFile(),
'currentPath' => $changeset->getFilename(),
'awayPaths' => $changeset->getAwayPaths(),
'oldProperties' => $changeset->getOldProperties(),
'newProperties' => $changeset->getNewProperties(),
'type' => $changeset->getChangeType(),
'fileType' => $changeset->getFileType(),
'commitHash' => null,
'addLines' => $changeset->getAddLines(),
'delLines' => $changeset->getDelLines(),
'hunks' => $hunks,
);
$changes[] = $change;
}
return $changes;
}
public function hasRevision() {
return $this->revision !== self::ATTACHABLE;
}
public function getRevision() {
return $this->assertAttached($this->revision);
}
public function attachRevision(DifferentialRevision $revision = null) {
$this->revision = $revision;
return $this;
}
public function attachProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key) {
return $this->assertAttachedKey($this->properties, $key);
}
public function hasDiffProperty($key) {
$properties = $this->getDiffProperties();
return array_key_exists($key, $properties);
}
public function attachDiffProperties(array $properties) {
$this->properties = $properties;
return $this;
}
public function getDiffProperties() {
return $this->assertAttached($this->properties);
}
public function attachBuildable(HarbormasterBuildable $buildable = null) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getBuildTargetPHIDs() {
$buildable = $this->getBuildable();
if (!$buildable) {
return array();
}
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
return $target_phids;
}
public function loadCoverageMap(PhabricatorUser $viewer) {
$target_phids = $this->getBuildTargetPHIDs();
if (!$target_phids) {
return array();
}
$unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$map = array();
foreach ($unit as $message) {
$coverage = $message->getProperty('coverage', array());
foreach ($coverage as $path => $coverage_data) {
$map[$path][] = $coverage_data;
}
}
foreach ($map as $path => $coverage_items) {
$map[$path] = ArcanistUnitTestResult::mergeCoverage($coverage_items);
}
return $map;
}
public function getURI() {
$id = $this->getID();
return "/differential/diff/{$id}/";
}
public function attachUnitMessages(array $unit_messages) {
$this->unitMessages = $unit_messages;
return $this;
}
public function getUnitMessages() {
return $this->assertAttached($this->unitMessages);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
if ($this->hasRevision()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->viewPolicy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->hasRevision()) {
return $this->getRevision()->hasAutomaticCapability($capability, $viewer);
}
return ($this->getAuthorPHID() == $viewer->getPHID());
}
public function describeAutomaticCapability($capability) {
if ($this->hasRevision()) {
return pht(
'This diff is attached to a revision, and inherits its policies.');
}
return pht('The author of a diff can see it.');
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->hasRevision()) {
$extended[] = array(
$this->getRevision(),
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
$container_phid = $this->getHarbormasterContainerPHID();
if ($container_phid) {
return $container_phid;
}
return $this->getHarbormasterBuildablePHID();
}
public function getHarbormasterBuildablePHID() {
return $this->getPHID();
}
public function getHarbormasterContainerPHID() {
if ($this->getRevisionID()) {
$revision = id(new DifferentialRevision())->load($this->getRevisionID());
if ($revision) {
return $revision->getPHID();
}
}
return null;
}
public function getBuildVariables() {
$results = array();
$results['buildable.diff'] = $this->getID();
if ($this->revisionID) {
$revision = $this->getRevision();
$results['buildable.revision'] = $revision->getID();
$repo = $revision->getRepository();
if ($repo) {
$results['repository.callsign'] = $repo->getCallsign();
$results['repository.phid'] = $repo->getPHID();
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
$results['repository.staging.uri'] = $repo->getStagingURI();
$results['repository.staging.ref'] = $this->getStagingRef();
}
}
return $results;
}
public function getAvailableBuildVariables() {
return array(
'buildable.diff' =>
pht('The differential diff ID, if applicable.'),
'buildable.revision' =>
pht('The differential revision ID, if applicable.'),
'repository.callsign' =>
pht('The callsign of the repository in Phabricator.'),
'repository.phid' =>
pht('The PHID of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
'repository.staging.uri' =>
pht('The URI of the staging repository.'),
'repository.staging.ref' =>
pht('The ref name for this change in the staging repository.'),
);
}
public function newBuildableEngine() {
return new DifferentialBuildableEngine();
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$diff_phid = $this->getPHID();
$repository_phid = $this->getRepositoryPHID();
if (!$repository_phid) {
throw new Exception(
pht(
'This diff ("%s") is not associated with a repository. A diff '.
'must belong to a tracked repository to be built by CircleCI.',
$diff_phid));
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") which '.
'could not be loaded.',
$diff_phid,
$repository_phid));
}
$staging_uri = $repository->getStagingURI();
if (!$staging_uri) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area configured. You must configure a '.
'Staging Area to use CircleCI integration.',
$diff_phid,
$repository_phid));
}
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$staging_uri);
if (!$path) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area ("%s") that is hosted on GitHub. '.
'CircleCI can only build from GitHub, so the Staging Area for '.
'the repository must be hosted there.',
$diff_phid,
$repository_phid,
$staging_uri));
}
return $staging_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'tag';
}
public function getCircleCIBuildIdentifier() {
$ref = $this->getStagingRef();
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
/* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */
public function getBuildkiteBranch() {
$ref = $this->getStagingRef();
// NOTE: Circa late January 2017, Buildkite fails with the error message
// "Tags have been disabled for this project" if we pass the "refs/tags/"
// prefix via the API and the project doesn't have GitHub tag builds
// enabled, even if GitHub builds are disabled. The tag builds fine
// without this prefix.
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
public function getBuildkiteCommit() {
return 'HEAD';
}
public function getStagingRef() {
// TODO: We're just hoping to get lucky. Instead, `arc` should store
// where it sent changes and we should only provide staging details
// if we reasonably believe they are accurate.
return 'refs/tags/phabricator/diff/'.$this->getID();
}
public function loadTargetBranch() {
// TODO: This is sketchy, but just eat the query cost until this can get
// cleaned up.
// For now, we're only returning a target if there's exactly one and it's
// a branch, since we don't support landing to more esoteric targets like
// tags yet.
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$this->getID(),
'arc:onto');
if (!$property) {
return null;
}
$data = $property->getData();
if (!$data) {
return null;
}
if (!is_array($data)) {
return null;
}
if (count($data) != 1) {
return null;
}
$onto = head($data);
if (!is_array($onto)) {
return null;
}
$type = idx($onto, 'type');
if ($type != 'branch') {
return null;
}
return idx($onto, 'name');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialDiffEditor();
}
public function 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) {
$engine->destroyObject($changeset);
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $prop) {
$prop->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('revisionPHID')
->setType('phid')
->setDescription(pht('Associated revision PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid')
->setDescription(pht('Associated repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('refs')
->setType('map<string, wild>')
->setDescription(pht('List of related VCS references.')),
);
}
public function getFieldValuesForConduit() {
$refs = array();
$branch = $this->getBranch();
if (strlen($branch)) {
$refs[] = array(
'type' => 'branch',
'name' => $branch,
);
}
$onto = $this->loadTargetBranch();
if (strlen($onto)) {
$refs[] = array(
'type' => 'onto',
'name' => $onto,
);
}
$base = $this->getSourceControlBaseRevision();
if (strlen($base)) {
$refs[] = array(
'type' => 'base',
'identifier' => $base,
);
}
$bookmark = $this->getBookmark();
if (strlen($bookmark)) {
$refs[] = array(
'type' => 'bookmark',
'name' => $bookmark,
);
}
$revision_phid = null;
if ($this->getRevisionID()) {
$revision_phid = $this->getRevision()->getPHID();
}
return array(
'revisionPHID' => $revision_phid,
'authorPHID' => $this->getAuthorPHID(),
'repositoryPHID' => $this->getRepositoryPHID(),
'refs' => $refs,
);
}
public function getConduitSearchAttachments() {
- return array();
+ return array(
+ id(new DifferentialCommitsSearchEngineAttachment())
+ ->setAttachmentKey('commits'),
+ );
}
-
}
diff --git a/src/applications/differential/storage/DifferentialHunk.php b/src/applications/differential/storage/DifferentialHunk.php
index 3defb3566..6bfb38b78 100644
--- a/src/applications/differential/storage/DifferentialHunk.php
+++ b/src/applications/differential/storage/DifferentialHunk.php
@@ -1,479 +1,485 @@
<?php
final class DifferentialHunk
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $changesetID;
protected $oldOffset;
protected $oldLen;
protected $newOffset;
protected $newLen;
protected $dataType;
protected $dataEncoding;
protected $dataFormat;
protected $data;
private $changeset;
private $splitLines;
private $structuredLines;
private $structuredFiles = array();
private $rawData;
private $forcedEncoding;
private $fileData;
const FLAG_LINES_ADDED = 1;
const FLAG_LINES_REMOVED = 2;
const FLAG_LINES_STABLE = 4;
const DATATYPE_TEXT = 'text';
const DATATYPE_FILE = 'file';
const DATAFORMAT_RAW = 'byte';
const DATAFORMAT_DEFLATED = 'gzde';
protected function getConfiguration() {
return array(
self::CONFIG_BINARY => array(
'data' => true,
),
self::CONFIG_COLUMN_SCHEMA => array(
'dataType' => 'bytes4',
'dataEncoding' => 'text16?',
'dataFormat' => 'bytes4',
'oldOffset' => 'uint32',
'oldLen' => 'uint32',
'newOffset' => 'uint32',
'newLen' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_changeset' => array(
'columns' => array('changesetID'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function getAddedLines() {
return $this->makeContent($include = '+');
}
public function getRemovedLines() {
return $this->makeContent($include = '-');
}
public function makeNewFile() {
return implode('', $this->makeContent($include = ' +'));
}
public function makeOldFile() {
return implode('', $this->makeContent($include = ' -'));
}
public function makeChanges() {
return implode('', $this->makeContent($include = '-+'));
}
public function getStructuredOldFile() {
return $this->getStructuredFile('-');
}
public function getStructuredNewFile() {
return $this->getStructuredFile('+');
}
private function getStructuredFile($kind) {
if ($kind !== '+' && $kind !== '-') {
throw new Exception(
pht(
'Structured file kind should be "+" or "-", got "%s".',
$kind));
}
if (!isset($this->structuredFiles[$kind])) {
if ($kind == '+') {
$number = $this->newOffset;
} else {
$number = $this->oldOffset;
}
$lines = $this->getStructuredLines();
// NOTE: We keep the "\ No newline at end of file" line if it appears
// after a line which is not excluded. For example, if we're constructing
// the "+" side of the diff, we want to ignore this one since it's
// relevant only to the "-" side of the diff:
//
// - x
// \ No newline at end of file
// + x
//
// ...but we want to keep this one:
//
// - x
// + x
// \ No newline at end of file
$file = array();
$keep = true;
foreach ($lines as $line) {
switch ($line['type']) {
case ' ':
case $kind:
$file[$number++] = $line;
$keep = true;
break;
case '\\':
if ($keep) {
// Strip the actual newline off the line's text.
$text = $file[$number - 1]['text'];
$text = rtrim($text, "\r\n");
$file[$number - 1]['text'] = $text;
$file[$number++] = $line;
$keep = false;
}
break;
default:
$keep = false;
break;
}
}
$this->structuredFiles[$kind] = $file;
}
return $this->structuredFiles[$kind];
}
public function getSplitLines() {
if ($this->splitLines === null) {
$this->splitLines = phutil_split_lines($this->getChanges());
}
return $this->splitLines;
}
public function getStructuredLines() {
if ($this->structuredLines === null) {
$lines = $this->getSplitLines();
$structured = array();
foreach ($lines as $line) {
if (empty($line[0])) {
// TODO: Can we just get rid of this?
continue;
}
$structured[] = array(
'type' => $line[0],
'text' => substr($line, 1),
);
}
$this->structuredLines = $structured;
}
return $this->structuredLines;
}
public function getContentWithMask($mask) {
$include = array();
if (($mask & self::FLAG_LINES_ADDED)) {
$include[] = '+';
}
if (($mask & self::FLAG_LINES_REMOVED)) {
$include[] = '-';
}
if (($mask & self::FLAG_LINES_STABLE)) {
$include[] = ' ';
}
$include = implode('', $include);
return implode('', $this->makeContent($include));
}
final private function makeContent($include) {
$lines = $this->getSplitLines();
$results = array();
$include_map = array();
for ($ii = 0; $ii < strlen($include); $ii++) {
$include_map[$include[$ii]] = true;
}
if (isset($include_map['+'])) {
$n = $this->newOffset;
} else {
$n = $this->oldOffset;
}
$use_next_newline = false;
foreach ($lines as $line) {
if (!isset($line[0])) {
continue;
}
if ($line[0] == '\\') {
if ($use_next_newline) {
$results[last_key($results)] = rtrim(end($results), "\n");
}
} else if (empty($include_map[$line[0]])) {
$use_next_newline = false;
} else {
$use_next_newline = true;
$results[$n] = substr($line, 1);
}
if ($line[0] == ' ' || isset($include_map[$line[0]])) {
$n++;
}
}
return $results;
}
public function getChangeset() {
return $this->assertAttached($this->changeset);
}
public function attachChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
return $this;
}
/* -( Storage )------------------------------------------------------------ */
public function setChanges($text) {
$this->rawData = $text;
$this->dataEncoding = $this->detectEncodingForStorage($text);
$this->dataType = self::DATATYPE_TEXT;
list($format, $data) = $this->formatDataForStorage($text);
$this->dataFormat = $format;
$this->data = $data;
return $this;
}
public function getChanges() {
return $this->getUTF8StringFromStorage(
$this->getRawData(),
nonempty($this->forcedEncoding, $this->getDataEncoding()));
}
public function forceEncoding($encoding) {
$this->forcedEncoding = $encoding;
return $this;
}
private function formatDataForStorage($data) {
$deflated = PhabricatorCaches::maybeDeflateData($data);
if ($deflated !== null) {
return array(self::DATAFORMAT_DEFLATED, $deflated);
}
return array(self::DATAFORMAT_RAW, $data);
}
+ public function getAutomaticDataFormat() {
+ // If the hunk is already stored deflated, just keep it deflated. This is
+ // mostly a performance improvement for "bin/differential migrate-hunk" so
+ // that we don't have to recompress all the stored hunks when looking for
+ // stray uncompressed hunks.
+ if ($this->dataFormat === self::DATAFORMAT_DEFLATED) {
+ return self::DATAFORMAT_DEFLATED;
+ }
+
+ list($format) = $this->formatDataForStorage($this->getRawData());
+
+ return $format;
+ }
+
public function saveAsText() {
$old_type = $this->getDataType();
$old_data = $this->getData();
- if ($old_type == self::DATATYPE_TEXT) {
- return $this;
- }
-
$raw_data = $this->getRawData();
$this->setDataType(self::DATATYPE_TEXT);
list($format, $data) = $this->formatDataForStorage($raw_data);
$this->setDataFormat($format);
$this->setData($data);
$result = $this->save();
$this->destroyData($old_type, $old_data);
return $result;
}
public function saveAsFile() {
$old_type = $this->getDataType();
$old_data = $this->getData();
- if ($old_type == self::DATATYPE_FILE) {
- return $this;
- }
-
$raw_data = $this->getRawData();
list($format, $data) = $this->formatDataForStorage($raw_data);
$this->setDataFormat($format);
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => 'differential-hunk',
'mime-type' => 'application/octet-stream',
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$this->setDataType(self::DATATYPE_FILE);
$this->setData($file->getPHID());
// NOTE: Because hunks don't have a PHID and we just load hunk data with
// the omnipotent viewer, we do not need to attach the file to anything.
$result = $this->save();
$this->destroyData($old_type, $old_data);
return $result;
}
private function getRawData() {
if ($this->rawData === null) {
$type = $this->getDataType();
$data = $this->getData();
switch ($type) {
case self::DATATYPE_TEXT:
// In this storage type, the changes are stored on the object.
$data = $data;
break;
case self::DATATYPE_FILE:
$data = $this->loadFileData();
break;
default:
throw new Exception(
pht('Hunk has unsupported data type "%s"!', $type));
}
$format = $this->getDataFormat();
switch ($format) {
case self::DATAFORMAT_RAW:
// In this format, the changes are stored as-is.
$data = $data;
break;
case self::DATAFORMAT_DEFLATED:
$data = PhabricatorCaches::inflateData($data);
break;
default:
throw new Exception(
pht('Hunk has unsupported data encoding "%s"!', $type));
}
$this->rawData = $data;
}
return $this->rawData;
}
private function loadFileData() {
if ($this->fileData === null) {
$type = $this->getDataType();
if ($type !== self::DATATYPE_FILE) {
throw new Exception(
pht(
'Unable to load file data for hunk with wrong data type ("%s").',
$type));
}
$file_phid = $this->getData();
$file = $this->loadRawFile($file_phid);
$data = $file->loadFileData();
$this->fileData = $data;
}
return $this->fileData;
}
private function loadRawFile($file_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->execute();
if (!$files) {
throw new Exception(
pht(
'Failed to load file ("%s") with hunk data.',
$file_phid));
}
$file = head($files);
return $file;
}
private function destroyData(
$type,
$data,
PhabricatorDestructionEngine $engine = null) {
if (!$engine) {
$engine = new PhabricatorDestructionEngine();
}
switch ($type) {
case self::DATATYPE_FILE:
$file = $this->loadRawFile($data);
$engine->destroyObject($file);
break;
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getChangeset()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getChangeset()->hasAutomaticCapability($capability, $viewer);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$type = $this->getDataType();
$data = $this->getData();
$this->destroyData($type, $data, $engine);
$this->delete();
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index 2f7cee110..9b169f3c6 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,1188 +1,1209 @@
<?php
final class DifferentialRevision extends DifferentialDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
HarbormasterBuildableInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorDraftInterface {
protected $title = '';
protected $status;
protected $summary = '';
protected $testPlan = '';
protected $authorPHID;
protected $lastReviewerPHID;
protected $lineCount = 0;
protected $attached = array();
protected $mailKey;
protected $branchName;
protected $repositoryPHID;
protected $activeDiffPHID;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $properties = array();
private $commits = self::ATTACHABLE;
private $activeDiff = self::ATTACHABLE;
private $diffIDs = self::ATTACHABLE;
private $hashes = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $reviewerStatus = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
private $forceMap = array();
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose';
const PROPERTY_DRAFT_HOLD = 'draft.hold';
const PROPERTY_SHOULD_BROADCAST = 'draft.broadcast';
const PROPERTY_LINES_ADDED = 'lines.added';
const PROPERTY_LINES_REMOVED = 'lines.removed';
const PROPERTY_BUILDABLES = 'buildables';
public static function initializeNewRevision(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
$initial_state = DifferentialRevisionStatus::DRAFT;
$should_broadcast = false;
} else {
$initial_state = DifferentialRevisionStatus::NEEDS_REVIEW;
$should_broadcast = true;
}
return id(new DifferentialRevision())
->setViewPolicy($view_policy)
->setAuthorPHID($actor->getPHID())
->attachRepository(null)
->attachActiveDiff(null)
->attachReviewers(array())
->setModernRevisionStatus($initial_state)
->setShouldBroadcast($should_broadcast);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'status' => 'text32',
'summary' => 'text',
'testPlan' => 'text',
'authorPHID' => 'phid?',
'lastReviewerPHID' => 'phid?',
'lineCount' => 'uint32?',
'mailKey' => 'bytes40',
'branchName' => 'text255?',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'repositoryPHID' => array(
'columns' => array('repositoryPHID'),
),
// If you (or a project you are a member of) is reviewing a significant
// fraction of the revisions on an install, the result set of open
// revisions may be smaller than the result set of revisions where you
// are a reviewer. In these cases, this key is better than keys on the
// edge table.
'key_status' => array(
'columns' => array('status', 'phid'),
),
),
) + parent::getConfiguration();
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function hasRevisionProperty($key) {
return array_key_exists($key, $this->properties);
}
public function getMonogram() {
$id = $this->getID();
return "D{$id}";
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function getHashes() {
return $this->assertAttached($this->hashes);
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
public function canReviewerForceAccept(
PhabricatorUser $viewer,
DifferentialReviewer $reviewer) {
if (!$reviewer->isPackage()) {
return false;
}
$map = $this->getReviewerForceAcceptMap($viewer);
if (!$map) {
return false;
}
if (isset($map[$reviewer->getReviewerPHID()])) {
return true;
}
return false;
}
private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
$fragment = $viewer->getCacheFragment();
if (!array_key_exists($fragment, $this->forceMap)) {
$map = $this->newReviewerForceAcceptMap($viewer);
$this->forceMap[$fragment] = $map;
}
return $this->forceMap[$fragment];
}
private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
if (!$diff) {
return null;
}
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return null;
}
$paths = array();
try {
$changesets = $diff->getChangesets();
} catch (Exception $ex) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs(array($diff))
->execute();
}
foreach ($changesets as $changeset) {
$paths[] = $changeset->getOwnersFilename();
}
if (!$paths) {
return null;
}
$reviewer_phids = array();
foreach ($this->getReviewers() as $reviewer) {
if (!$reviewer->isPackage()) {
continue;
}
$reviewer_phids[] = $reviewer->getReviewerPHID();
}
if (!$reviewer_phids) {
return null;
}
// Load all the reviewing packages which have control over some of the
// paths in the change. These are packages which the actor may be able
// to force-accept on behalf of.
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withPHIDs($reviewer_phids)
->withControl($repository_phid, $paths);
$control_packages = $control_query->execute();
if (!$control_packages) {
return null;
}
// Load all the packages which have potential control over some of the
// paths in the change and are owned by the actor. These are packages
// which the actor may be able to use their authority over to gain the
// ability to force-accept for other packages. This query doesn't apply
// dominion rules yet, and we'll bypass those rules later on.
$authority_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->withControl($repository_phid, $paths);
$authority_packages = $authority_query->execute();
if (!$authority_packages) {
return null;
}
$authority_packages = mpull($authority_packages, null, 'getPHID');
// Build a map from each path in the revision to the reviewer packages
// which control it.
$control_map = array();
foreach ($paths as $path) {
$control_packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$path);
// Remove packages which the viewer has authority over. We don't need
// to check these for force-accept because they can just accept them
// normally.
$control_packages = mpull($control_packages, null, 'getPHID');
foreach ($control_packages as $phid => $control_package) {
if (isset($authority_packages[$phid])) {
unset($control_packages[$phid]);
}
}
if (!$control_packages) {
continue;
}
$control_map[$path] = $control_packages;
}
if (!$control_map) {
return null;
}
// From here on out, we only care about paths which we have at least one
// controlling package for.
$paths = array_keys($control_map);
// Now, build a map from each path to the packages which would control it
// if there were no dominion rules.
$authority_map = array();
foreach ($paths as $path) {
$authority_packages = $authority_query->getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = true);
$authority_map[$path] = mpull($authority_packages, null, 'getPHID');
}
// For each path, find the most general package that the viewer has
// authority over. For example, we'll prefer a package that owns "/" to a
// package that owns "/src/".
$force_map = array();
foreach ($authority_map as $path => $package_map) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
// Find the package that we have authority over which has the most
// general match for this path.
$best_match = null;
$best_package = null;
foreach ($package_map as $package_phid => $package) {
$package_paths = $package->getPathsForRepository($repository_phid);
foreach ($package_paths as $package_path) {
// NOTE: A strength of 0 means "no match". A strength of 1 means
// that we matched "/", so we can not possibly find another stronger
// match.
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength < $best_match || !$best_package) {
$best_match = $strength;
$best_package = $package;
if ($strength == 1) {
break 2;
}
}
}
}
if ($best_package) {
$force_map[$path] = array(
'strength' => $best_match,
'package' => $best_package,
);
}
}
// For each path which the viewer owns a package for, find other packages
// which that authority can be used to force-accept. Once we find a way to
// force-accept a package, we don't need to keep looking.
$has_control = array();
foreach ($force_map as $path => $spec) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$authority_strength = $spec['strength'];
$control_packages = $control_map[$path];
foreach ($control_packages as $control_phid => $control_package) {
if (isset($has_control[$control_phid])) {
continue;
}
$control_paths = $control_package->getPathsForRepository(
$repository_phid);
foreach ($control_paths as $control_path) {
$strength = $control_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength > $authority_strength) {
$authority = $spec['package'];
$has_control[$control_phid] = array(
'authority' => $authority,
'phid' => $authority->getPHID(),
);
break;
}
}
}
}
// Return a map from packages which may be force accepted to the packages
// which permit that forced acceptance.
return ipull($has_control, 'phid');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A revision's author (which effectively means "owner" after we added
// commandeering) can always view and edit it.
$author_phid = $this->getAuthorPHID();
if ($author_phid) {
if ($user->getPHID() == $author_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
$description = array(
pht('The owner of a revision can always view and edit it.'),
);
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$description[] = pht(
'If a revision belongs to a repository, other users must be able '.
'to view the repository in order to view the revision.');
break;
}
return $description;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$repository_phid = $this->getRepositoryPHID();
$repository = $this->getRepository();
// Try to use the object if we have it, since it will save us some
// data fetching later on. In some cases, we might not have it.
$repository_ref = nonempty($repository, $repository_phid);
if ($repository_ref) {
$extended[] = array(
$repository_ref,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
public function getReviewers() {
return $this->assertAttached($this->reviewerStatus);
}
public function attachReviewers(array $reviewers) {
assert_instances_of($reviewers, 'DifferentialReviewer');
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
$this->reviewerStatus = $reviewers;
return $this;
}
public function hasAttachedReviewers() {
return ($this->reviewerStatus !== self::ATTACHABLE);
}
public function getReviewerPHIDs() {
$reviewers = $this->getReviewers();
return mpull($reviewers, 'getReviewerPHID');
}
public function getReviewerPHIDsForEdit() {
$reviewers = $this->getReviewers();
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$value = array();
foreach ($reviewers as $reviewer) {
$phid = $reviewer->getReviewerPHID();
if ($reviewer->getReviewerStatus() == $status_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
return $value;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function setModernRevisionStatus($status) {
return $this->setStatus($status);
}
public function getModernRevisionStatus() {
return $this->getStatus();
}
public function getLegacyRevisionStatus() {
return $this->getStatusObject()->getLegacyKey();
}
public function isClosed() {
return $this->getStatusObject()->isClosedStatus();
}
public function isAbandoned() {
return $this->getStatusObject()->isAbandoned();
}
public function isAccepted() {
return $this->getStatusObject()->isAccepted();
}
public function isNeedsReview() {
return $this->getStatusObject()->isNeedsReview();
}
public function isNeedsRevision() {
return $this->getStatusObject()->isNeedsRevision();
}
public function isChangePlanned() {
return $this->getStatusObject()->isChangePlanned();
}
public function isPublished() {
return $this->getStatusObject()->isPublished();
}
public function isDraft() {
return $this->getStatusObject()->isDraft();
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function getStatusIconColor() {
return $this->getStatusObject()->getIconColor();
}
public function getStatusTagColor() {
return $this->getStatusObject()->getTagColor();
}
public function getStatusObject() {
$status = $this->getStatus();
return DifferentialRevisionStatus::newForStatus($status);
}
public function getFlag(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
}
public function attachFlag(
PhabricatorUser $viewer,
PhabricatorFlag $flag = null) {
$this->flags[$viewer->getPHID()] = $flag;
return $this;
}
public function getHasDraft(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
}
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
return $this;
}
public function getHoldAsDraft() {
return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false);
}
public function setHoldAsDraft($hold) {
return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold);
}
public function getShouldBroadcast() {
return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true);
}
public function setShouldBroadcast($should_broadcast) {
return $this->setProperty(
self::PROPERTY_SHOULD_BROADCAST,
$should_broadcast);
}
public function setAddedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_ADDED, $count);
}
public function getAddedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_ADDED);
}
public function setRemovedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count);
}
public function getRemovedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_REMOVED);
}
public function hasLineCounts() {
// This data was not populated on older revisions, so it may not be
// present on all revisions.
return isset($this->properties[self::PROPERTY_LINES_ADDED]);
}
public function getRevisionScaleGlyphs() {
$add = $this->getAddedLineCount();
$rem = $this->getRemovedLineCount();
$all = ($add + $rem);
if (!$all) {
return ' ';
}
$map = array(
20 => 2,
50 => 3,
150 => 4,
375 => 5,
1000 => 6,
2500 => 7,
);
$n = 1;
foreach ($map as $size => $count) {
if ($size <= $all) {
$n = $count;
} else {
break;
}
}
$add_n = (int)ceil(($add / $all) * $n);
$rem_n = (int)ceil(($rem / $all) * $n);
while ($add_n + $rem_n > $n) {
if ($add_n > 1) {
$add_n--;
} else {
$rem_n--;
}
}
return
str_repeat('+', $add_n).
str_repeat('-', $rem_n).
str_repeat(' ', (7 - $n));
}
public function getBuildableStatus($phid) {
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
if (!is_array($buildables)) {
$buildables = array();
}
$buildable = idx($buildables, $phid);
if (!is_array($buildable)) {
$buildable = array();
}
return idx($buildable, 'status');
}
public function setBuildableStatus($phid, $status) {
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
if (!is_array($buildables)) {
$buildables = array();
}
$buildable = idx($buildables, $phid);
if (!is_array($buildable)) {
$buildable = array();
}
$buildable['status'] = $status;
$buildables[$phid] = $buildable;
return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables);
}
public function newBuildableStatus(PhabricatorUser $viewer, $phid) {
// For Differential, we're ignoring autobuilds (local lint and unit)
// when computing build status. Differential only cares about remote
// builds when making publishing and undrafting decisions.
$builds = $this->loadImpactfulBuildsForBuildablePHIDs(
$viewer,
array($phid));
return $this->newBuildableStatusForBuilds($builds);
}
public function newBuildableStatusForBuilds(array $builds) {
// If we have nothing but passing builds, the buildable passes.
if (!$builds) {
return HarbormasterBuildableStatus::STATUS_PASSED;
}
// If we have any completed, non-passing builds, the buildable fails.
foreach ($builds as $build) {
if ($build->isComplete()) {
return HarbormasterBuildableStatus::STATUS_FAILED;
}
}
// Otherwise, we're still waiting for the build to pass or fail.
return null;
}
public function loadImpactfulBuilds(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
// NOTE: We can't use `withContainerPHIDs()` here because the container
// update in Harbormaster is not synchronous.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($diff->getPHID()))
->withManualBuildables(false)
->execute();
if (!$buildables) {
return array();
}
return $this->loadImpactfulBuildsForBuildablePHIDs(
$viewer,
mpull($buildables, 'getPHID'));
}
private function loadImpactfulBuildsForBuildablePHIDs(
PhabricatorUser $viewer,
array $phids) {
return id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs($phids)
->withAutobuilds(false)
->withBuildStatuses(
array(
HarbormasterBuildStatus::STATUS_INACTIVE,
HarbormasterBuildStatus::STATUS_PENDING,
HarbormasterBuildStatus::STATUS_BUILDING,
HarbormasterBuildStatus::STATUS_FAILED,
HarbormasterBuildStatus::STATUS_ABORTED,
HarbormasterBuildStatus::STATUS_ERROR,
HarbormasterBuildStatus::STATUS_PAUSED,
HarbormasterBuildStatus::STATUS_DEADLOCKED,
))
->execute();
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
return $this->getHarbormasterContainerPHID();
}
public function getHarbormasterBuildablePHID() {
return $this->loadActiveDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
public function newBuildableEngine() {
return new DifferentialBuildableEngine();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
if ($phid == $this->getAuthorPHID()) {
return true;
}
// TODO: This only happens when adding or removing CCs, and is safe from a
// policy perspective, but the subscription pathway should have some
// opportunity to load this data properly. For now, this is the only case
// where implicit subscription is not an intrinsic property of the object.
if ($this->reviewerStatus == self::ATTACHABLE) {
$reviewers = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needReviewers(true)
->executeOne()
->getReviewers();
} else {
$reviewers = $this->getReviewers();
}
foreach ($reviewers as $reviewer) {
if ($reviewer->getReviewerPHID() !== $phid) {
continue;
}
if ($reviewer->isResigned()) {
continue;
}
return true;
}
return false;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('differential.fields');
}
public function getCustomFieldBaseClass() {
return 'DifferentialCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
$viewer = $request->getViewer();
$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];
$old_ids = $request->getStr('old', idx($render_data, 'old'));
$new_ids = $request->getStr('new', idx($render_data, 'new'));
$old_ids = array_filter(explode(',', $old_ids));
$new_ids = array_filter(explode(',', $new_ids));
$type_inline = DifferentialTransaction::TYPE_INLINE;
$changeset_ids = array_merge($old_ids, $new_ids);
$inlines = array();
foreach ($timeline->getTransactions() as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction->getComment();
$changeset_ids[] = $xaction->getComment()->getChangesetID();
}
}
if ($changeset_ids) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($request->getUser())
->withIDs($changeset_ids)
->execute();
$changesets = mpull($changesets, null, 'getID');
} else {
$changesets = array();
}
foreach ($inlines as $key => $inline) {
$inlines[$key] = DifferentialInlineComment::newFromModernComment(
$inline);
}
$query = id(new DifferentialInlineCommentQuery())
->needHidden(true)
->setViewer($viewer);
// NOTE: This is a bit sketchy: this method adjusts the inlines as a
// side effect, which means it will ultimately adjust the transaction
// comments and affect timeline rendering.
$query->adjustInlinesForChangesets(
$inlines,
array_select_keys($changesets, $old_ids),
array_select_keys($changesets, $new_ids),
$this);
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($engine->getViewer())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($diffs as $diff) {
$engine->destroyObject($diff);
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
// we have to do paths a little differently as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DifferentialRevisionFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new DifferentialRevisionFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The revision title.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about revision status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid?')
->setDescription(pht('Revision repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('diffPHID')
->setType('phid')
->setDescription(pht('Active diff PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('summary')
->setType('string')
->setDescription(pht('Revision summary.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('testPlan')
+ ->setType('string')
+ ->setDescription(pht('Revision test plan.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('isDraft')
+ ->setType('bool')
+ ->setDescription(
+ pht(
+ 'True if this revision is in any draft state, and thus not '.
+ 'notifying reviewers and subscribers about changes.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('holdAsDraft')
+ ->setType('bool')
+ ->setDescription(
+ pht(
+ 'True if this revision is being held as a draft. It will not be '.
+ 'automatically submitted for review even if tests pass.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getStatusObject();
$status_info = array(
'value' => $status->getKey(),
'name' => $status->getDisplayName(),
'closed' => $status->isClosedStatus(),
'color.ansi' => $status->getANSIColor(),
);
return array(
'title' => $this->getTitle(),
'authorPHID' => $this->getAuthorPHID(),
'status' => $status_info,
'repositoryPHID' => $this->getRepositoryPHID(),
'diffPHID' => $this->getActiveDiffPHID(),
'summary' => $this->getSummary(),
+ 'testPlan' => $this->getTestPlan(),
+ 'isDraft' => !$this->getShouldBroadcast(),
+ 'holdAsDraft' => (bool)$this->getHoldAsDraft(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialReviewersSearchEngineAttachment())
->setAttachmentKey('reviewers'),
);
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DifferentialRevisionDraftEngine();
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php
index 59f73b546..14de553e5 100644
--- a/src/applications/differential/view/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/DifferentialChangesetListView.php
@@ -1,412 +1,424 @@
<?php
final class DifferentialChangesetListView extends AphrontView {
private $changesets = array();
private $visibleChangesets = array();
private $references = array();
private $inlineURI;
private $renderURI = '/differential/changeset/';
private $whitespace;
private $background;
private $header;
+ private $isStandalone;
private $standaloneURI;
private $leftRawFileURI;
private $rightRawFileURI;
private $inlineListURI;
private $symbolIndexes = array();
private $repository;
private $branch;
private $diff;
private $vsMap = array();
private $title;
private $parser;
public function setParser(DifferentialChangesetParser $parser) {
$this->parser = $parser;
return $this;
}
public function getParser() {
return $this->parser;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
private function getTitle() {
return $this->title;
}
public function setBranch($branch) {
$this->branch = $branch;
return $this;
}
private function getBranch() {
return $this->branch;
}
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function setVisibleChangesets($visible_changesets) {
$this->visibleChangesets = $visible_changesets;
return $this;
}
public function setInlineCommentControllerURI($uri) {
$this->inlineURI = $uri;
return $this;
}
public function setInlineListURI($uri) {
$this->inlineListURI = $uri;
return $this;
}
public function getInlineListURI() {
return $this->inlineListURI;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function setRenderingReferences(array $references) {
$this->references = $references;
return $this;
}
public function setSymbolIndexes(array $indexes) {
$this->symbolIndexes = $indexes;
return $this;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function setVsMap(array $vs_map) {
$this->vsMap = $vs_map;
return $this;
}
public function getVsMap() {
return $this->vsMap;
}
public function setStandaloneURI($uri) {
$this->standaloneURI = $uri;
return $this;
}
public function setRawFileURIs($l, $r) {
$this->leftRawFileURI = $l;
$this->rightRawFileURI = $r;
return $this;
}
+ public function setIsStandalone($is_standalone) {
+ $this->isStandalone = $is_standalone;
+ return $this;
+ }
+
+ public function getIsStandalone() {
+ return $this->isStandalone;
+ }
+
public function setBackground($background) {
$this->background = $background;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function render() {
$viewer = $this->getViewer();
$this->requireResource('differential-changeset-view-css');
$changesets = $this->changesets;
$renderer = DifferentialChangesetParser::getDefaultRendererForViewer(
$viewer);
$output = array();
$ids = array();
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
$ref = $this->references[$key];
$detail = id(new DifferentialChangesetDetailView())
->setUser($viewer);
$uniq_id = 'diff-'.$changeset->getAnchorName();
$detail->setID($uniq_id);
$view_options = $this->renderViewOptionsDropdown(
$detail,
$ref,
$changeset);
$detail->setChangeset($changeset);
$detail->addButton($view_options);
$detail->setSymbolIndex(idx($this->symbolIndexes, $key));
$detail->setVsChangesetID(idx($this->vsMap, $changeset->getID()));
$detail->setEditable(true);
$detail->setRenderingRef($ref);
$detail->setRenderURI($this->renderURI);
$detail->setWhitespace($this->whitespace);
$detail->setRenderer($renderer);
if ($this->getParser()) {
$detail->appendChild($this->getParser()->renderChangeset());
$detail->setLoaded(true);
} else {
$detail->setAutoload(isset($this->visibleChangesets[$key]));
if (isset($this->visibleChangesets[$key])) {
$load = pht('Loading...');
} else {
$load = javelin_tag(
'a',
array(
'class' => 'button button-grey',
'href' => '#'.$uniq_id,
'sigil' => 'differential-load',
'meta' => array(
'id' => $detail->getID(),
'kill' => true,
),
'mustcapture' => true,
),
pht('Load File'));
}
$detail->appendChild(
phutil_tag(
'div',
array(
'id' => $uniq_id,
),
phutil_tag(
'div',
array('class' => 'differential-loading'),
$load)));
}
$output[] = $detail->render();
$ids[] = $detail->getID();
}
$this->requireResource('aphront-tooltip-css');
$this->initBehavior(
'differential-populate',
array(
'changesetViewIDs' => $ids,
'inlineURI' => $this->inlineURI,
'inlineListURI' => $this->inlineListURI,
+ 'isStandalone' => $this->getIsStandalone(),
'pht' => array(
'Open in Editor' => pht('Open in Editor'),
'Show All Context' => pht('Show All Context'),
'All Context Shown' => pht('All Context Shown'),
"Can't Toggle Unloaded File" => pht("Can't Toggle Unloaded File"),
'Expand File' => pht('Expand File'),
'Collapse File' => pht('Collapse File'),
'Browse in Diffusion' => pht('Browse in Diffusion'),
'View Standalone' => pht('View Standalone'),
'Show Raw File (Left)' => pht('Show Raw File (Left)'),
'Show Raw File (Right)' => pht('Show Raw File (Right)'),
'Configure Editor' => pht('Configure Editor'),
'Load Changes' => pht('Load Changes'),
'View Side-by-Side' => pht('View Side-by-Side'),
'View Unified' => pht('View Unified'),
'Change Text Encoding...' => pht('Change Text Encoding...'),
'Highlight As...' => pht('Highlight As...'),
'Loading...' => pht('Loading...'),
'Editing Comment' => pht('Editing Comment'),
'Jump to next change.' => pht('Jump to next change.'),
'Jump to previous change.' => pht('Jump to previous change.'),
'Jump to next file.' => pht('Jump to next file.'),
'Jump to previous file.' => pht('Jump to previous file.'),
'Jump to next inline comment.' => pht('Jump to next inline comment.'),
'Jump to previous inline comment.' =>
pht('Jump to previous inline comment.'),
'Jump to the table of contents.' =>
pht('Jump to the table of contents.'),
'Edit selected inline comment.' =>
pht('Edit selected inline comment.'),
'You must select a comment to edit.' =>
pht('You must select a comment to edit.'),
'Reply to selected inline comment or change.' =>
pht('Reply to selected inline comment or change.'),
'You must select a comment or change to reply to.' =>
pht('You must select a comment or change to reply to.'),
'Reply and quote selected inline comment.' =>
pht('Reply and quote selected inline comment.'),
'Mark or unmark selected inline comment as done.' =>
pht('Mark or unmark selected inline comment as done.'),
'You must select a comment to mark done.' =>
pht('You must select a comment to mark done.'),
'Collapse or expand inline comment.' =>
pht('Collapse or expand inline comment.'),
'You must select a comment to hide.' =>
pht('You must select a comment to hide.'),
'Jump to next inline comment, including collapsed comments.' =>
pht('Jump to next inline comment, including collapsed comments.'),
'Jump to previous inline comment, including collapsed comments.' =>
pht('Jump to previous inline comment, including collapsed comments.'),
'This file content has been collapsed.' =>
pht('This file content has been collapsed.'),
'Show Content' => pht('Show Content'),
'Hide or show the current file.' =>
pht('Hide or show the current file.'),
'You must select a file to hide or show.' =>
pht('You must select a file to hide or show.'),
'Unsaved' => pht('Unsaved'),
'Unsubmitted' => pht('Unsubmitted'),
'Comments' => pht('Comments'),
'Hide "Done" Inlines' => pht('Hide "Done" Inlines'),
'Hide Collapsed Inlines' => pht('Hide Collapsed Inlines'),
'Hide Older Inlines' => pht('Hide Older Inlines'),
'Hide All Inlines' => pht('Hide All Inlines'),
'Show All Inlines' => pht('Show All Inlines'),
'List Inline Comments' => pht('List Inline Comments'),
+ 'Display Options' => pht('Display Options'),
'Hide or show all inline comments.' =>
pht('Hide or show all inline comments.'),
'Finish editing inline comments before changing display modes.' =>
pht('Finish editing inline comments before changing display modes.'),
),
));
if ($this->header) {
$header = $this->header;
} else {
$header = id(new PHUIHeaderView())
->setHeader($this->getTitle());
}
$content = phutil_tag(
'div',
array(
'class' => 'differential-review-stage',
'id' => 'differential-review-stage',
),
$output);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground($this->background)
->setCollapsed(true)
->appendChild($content);
return $object_box;
}
private function renderViewOptionsDropdown(
DifferentialChangesetDetailView $detail,
$ref,
DifferentialChangeset $changeset) {
$viewer = $this->getViewer();
$meta = array();
$qparams = array(
'ref' => $ref,
'whitespace' => $this->whitespace,
);
if ($this->standaloneURI) {
$uri = new PhutilURI($this->standaloneURI);
$uri->setQueryParams($uri->getQueryParams() + $qparams);
$meta['standaloneURI'] = (string)$uri;
}
$repository = $this->repository;
if ($repository) {
try {
$meta['diffusionURI'] =
(string)$repository->getDiffusionBrowseURIForPath(
$viewer,
$changeset->getAbsoluteRepositoryPath($repository, $this->diff),
idx($changeset->getMetadata(), 'line:first'),
$this->getBranch());
} catch (DiffusionSetupException $e) {
// Ignore
}
}
$change = $changeset->getChangeType();
if ($this->leftRawFileURI) {
if ($change != DifferentialChangeType::TYPE_ADD) {
$uri = new PhutilURI($this->leftRawFileURI);
$uri->setQueryParams($uri->getQueryParams() + $qparams);
$meta['leftURI'] = (string)$uri;
}
}
if ($this->rightRawFileURI) {
if ($change != DifferentialChangeType::TYPE_DELETE &&
$change != DifferentialChangeType::TYPE_MULTICOPY) {
$uri = new PhutilURI($this->rightRawFileURI);
$uri->setQueryParams($uri->getQueryParams() + $qparams);
$meta['rightURI'] = (string)$uri;
}
}
if ($viewer && $repository) {
$path = ltrim(
$changeset->getAbsoluteRepositoryPath($repository, $this->diff),
'/');
$line = idx($changeset->getMetadata(), 'line:first', 1);
$editor_link = $viewer->loadEditorLink($path, $line, $repository);
if ($editor_link) {
$meta['editor'] = $editor_link;
} else {
$meta['editorConfigure'] = '/settings/panel/display/';
}
}
$meta['containerID'] = $detail->getID();
return id(new PHUIButtonView())
->setTag('a')
->setText(pht('View Options'))
->setIcon('fa-bars')
->setColor(PHUIButtonView::GREY)
->setHref(idx($meta, 'detailURI', '#'))
->setMetadata($meta)
->addSigil('differential-view-options');
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php
index 4c69d7627..e53bccb89 100644
--- a/src/applications/differential/view/DifferentialRevisionListView.php
+++ b/src/applications/differential/view/DifferentialRevisionListView.php
@@ -1,253 +1,261 @@
<?php
/**
* Render a table of Differential revisions.
*/
final class DifferentialRevisionListView extends AphrontView {
private $revisions = array();
private $header;
private $noDataString;
private $noBox;
private $background = null;
private $unlandedDependencies = array();
public function setUnlandedDependencies(array $unlanded_dependencies) {
$this->unlandedDependencies = $unlanded_dependencies;
return $this;
}
public function getUnlandedDependencies() {
return $this->unlandedDependencies;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$this->revisions = $revisions;
return $this;
}
public function setNoBox($box) {
$this->noBox = $box;
return $this;
}
public function setBackground($background) {
$this->background = $background;
return $this;
}
public function render() {
$viewer = $this->getViewer();
$this->initBehavior('phabricator-tooltips', array());
$this->requireResource('aphront-tooltip-css');
$reviewer_limit = 7;
$reviewer_phids = array();
$reviewer_more = array();
$handle_phids = array();
foreach ($this->revisions as $key => $revision) {
$reviewers = $revision->getReviewers();
if (count($reviewers) > $reviewer_limit) {
$reviewers = array_slice($reviewers, 0, $reviewer_limit);
$reviewer_more[$key] = true;
} else {
$reviewer_more[$key] = false;
}
$phids = mpull($reviewers, 'getReviewerPHID');
$reviewer_phids[$key] = $phids;
foreach ($phids as $phid) {
$handle_phids[$phid] = $phid;
}
$author_phid = $revision->getAuthorPHID();
$handle_phids[$author_phid] = $author_phid;
}
$handles = $viewer->loadHandles($handle_phids);
$list = new PHUIObjectItemListView();
foreach ($this->revisions as $key => $revision) {
$item = id(new PHUIObjectItemView())
->setViewer($viewer);
$icons = array();
$phid = $revision->getPHID();
$flag = $revision->getFlag($viewer);
if ($flag) {
$flag_class = PhabricatorFlagColor::getCSSClass($flag->getColor());
$icons['flag'] = phutil_tag(
'div',
array(
'class' => 'phabricator-flag-icon '.$flag_class,
),
'');
}
$modified = $revision->getDateModified();
if (isset($icons['flag'])) {
$item->addHeadIcon($icons['flag']);
}
$item->setObjectName($revision->getMonogram());
$item->setHeader($revision->getTitle());
$item->setHref($revision->getURI());
$size = $this->renderRevisionSize($revision);
if ($size !== null) {
$item->addAttribute($size);
}
if ($revision->getHasDraft($viewer)) {
$draft = id(new PHUIIconView())
->setIcon('fa-comment yellow')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Unsubmitted Comments'),
));
$item->addAttribute($draft);
}
$author_handle = $handles[$revision->getAuthorPHID()];
$item->addByline(pht('Author: %s', $author_handle->renderLink()));
$unlanded = idx($this->unlandedDependencies, $phid);
if ($unlanded) {
$item->addAttribute(
array(
id(new PHUIIconView())->setIcon('fa-chain-broken', 'red'),
' ',
pht('Open Dependencies'),
));
}
$more = null;
if ($reviewer_more[$key]) {
$more = pht(', ...');
} else {
$more = null;
}
if ($reviewer_phids[$key]) {
$item->addAttribute(
array(
pht('Reviewers:'),
' ',
$viewer->renderHandleList($reviewer_phids[$key])
->setAsInline(true),
$more,
));
} else {
$item->addAttribute(phutil_tag('em', array(), pht('No Reviewers')));
}
$item->setEpoch($revision->getDateModified());
if ($revision->isClosed()) {
$item->setDisabled(true);
}
$icon = $revision->getStatusIcon();
$color = $revision->getStatusIconColor();
$item->setStatusIcon(
"{$icon} {$color}",
$revision->getStatusDisplayName());
$list->addItem($item);
}
$list->setNoDataString($this->noDataString);
if ($this->header && !$this->noBox) {
$list->setFlush(true);
$list = id(new PHUIObjectBoxView())
->setBackground($this->background)
->setObjectList($list);
if ($this->header instanceof PHUIHeaderView) {
$list->setHeader($this->header);
} else {
$list->setHeaderText($this->header);
}
} else {
$list->setHeader($this->header);
}
return $list;
}
private function renderRevisionSize(DifferentialRevision $revision) {
if (!$revision->hasLineCounts()) {
return null;
}
$size = array();
$glyphs = $revision->getRevisionScaleGlyphs();
$plus_count = 0;
for ($ii = 0; $ii < 7; $ii++) {
$c = $glyphs[$ii];
switch ($c) {
case '+':
$size[] = id(new PHUIIconView())
->setIcon('fa-plus');
$plus_count++;
break;
case '-':
$size[] = id(new PHUIIconView())
->setIcon('fa-minus');
break;
default:
$size[] = id(new PHUIIconView())
->setIcon('fa-square-o invisible');
break;
}
}
$n = $revision->getAddedLineCount() + $revision->getRemovedLineCount();
$classes = array();
$classes[] = 'differential-revision-size';
+ $tip = array();
+ $tip[] = pht('%s Lines', new PhutilNumber($n));
+
if ($plus_count <= 1) {
$classes[] = 'differential-revision-small';
+ $tip[] = pht('Smaller Change');
}
if ($plus_count >= 4) {
$classes[] = 'differential-revision-large';
+ $tip[] = pht('Larger Change');
}
+ $tip = phutil_implode_html(" \xC2\xB7 ", $tip);
+
return javelin_tag(
'span',
array(
'class' => implode(' ', $classes),
'sigil' => 'has-tooltip',
'meta' => array(
- 'tip' => pht('%s Lines', new PhutilNumber($n)),
+ 'tip' => $tip,
'align' => 'E',
+ 'size' => 400,
),
),
$size);
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
index bdac86067..a77b320d8 100644
--- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
+++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
@@ -1,439 +1,439 @@
<?php
final class DifferentialRevisionUpdateHistoryView extends AphrontView {
private $diffs = array();
private $selectedVersusDiffID;
private $selectedDiffID;
private $selectedWhitespace;
private $commitsForLinks = array();
private $unitStatus = array();
public function setDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs;
return $this;
}
public function setSelectedVersusDiffID($id) {
$this->selectedVersusDiffID = $id;
return $this;
}
public function setSelectedDiffID($id) {
$this->selectedDiffID = $id;
return $this;
}
public function setSelectedWhitespace($whitespace) {
$this->selectedWhitespace = $whitespace;
return $this;
}
public function setCommitsForLinks(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commitsForLinks = $commits;
return $this;
}
public function setDiffUnitStatuses(array $unit_status) {
$this->unitStatus = $unit_status;
return $this;
}
public function render() {
$this->requireResource('differential-core-view-css');
$this->requireResource('differential-revision-history-css');
$data = array(
array(
'name' => pht('Base'),
'id' => null,
'desc' => pht('Base'),
'age' => null,
'obj' => null,
),
);
$seq = 0;
foreach ($this->diffs as $diff) {
$data[] = array(
'name' => pht('Diff %d', ++$seq),
'id' => $diff->getID(),
'desc' => $diff->getDescription(),
'age' => $diff->getDateCreated(),
'obj' => $diff,
);
}
$max_id = $diff->getID();
$revision_id = $diff->getRevisionID();
$idx = 0;
$rows = array();
$disable = false;
$radios = array();
$last_base = null;
$rowc = array();
foreach ($data as $row) {
$diff = $row['obj'];
$name = $row['name'];
$id = $row['id'];
$old_class = false;
$new_class = false;
if ($id) {
$new_checked = ($this->selectedDiffID == $id);
$new = javelin_tag(
'input',
array(
'type' => 'radio',
'name' => 'id',
'value' => $id,
'checked' => $new_checked ? 'checked' : null,
'sigil' => 'differential-new-radio',
));
if ($new_checked) {
$new_class = true;
$disable = true;
}
$new = phutil_tag(
'div',
array(
'class' => 'differential-update-history-radio',
),
$new);
} else {
$new = null;
}
if ($max_id != $id) {
$uniq = celerity_generate_unique_node_id();
$old_checked = ($this->selectedVersusDiffID == $id);
$old = phutil_tag(
'input',
array(
'type' => 'radio',
'name' => 'vs',
'value' => $id,
'id' => $uniq,
'checked' => $old_checked ? 'checked' : null,
'disabled' => $disable ? 'disabled' : null,
));
$radios[] = $uniq;
if ($old_checked) {
$old_class = true;
}
$old = phutil_tag(
'div',
array(
'class' => 'differential-update-history-radio',
),
$old);
} else {
$old = null;
}
$desc = $row['desc'];
if ($row['age']) {
$age = phabricator_datetime($row['age'], $this->getUser());
} else {
$age = null;
}
if ($diff) {
$unit_status = idx(
$this->unitStatus,
$diff->getPHID(),
$diff->getUnitStatus());
$lint = self::renderDiffLintStar($row['obj']);
$lint = phutil_tag(
'div',
array(
'class' => 'lintunit-star',
'title' => self::getDiffLintMessage($diff),
),
$lint);
$unit = self::renderDiffUnitStar($unit_status);
$unit = phutil_tag(
'div',
array(
'class' => 'lintunit-star',
'title' => self::getDiffUnitMessage($unit_status),
),
$unit);
$base = $this->renderBaseRevision($diff);
} else {
$lint = null;
$unit = null;
$base = null;
}
if ($last_base !== null && $base !== $last_base) {
// TODO: Render some kind of notice about rebases.
}
$last_base = $base;
if ($revision_id) {
$id_link = phutil_tag(
'a',
array(
'href' => '/D'.$revision_id.'?id='.$id,
),
$id);
} else {
$id_link = phutil_tag(
'a',
array(
'href' => '/differential/diff/'.$id.'/',
),
$id);
}
$rows[] = array(
$name,
$id_link,
$base,
$desc,
$age,
$lint,
$unit,
$old,
$new,
);
$classes = array();
if ($old_class) {
$classes[] = 'differential-update-history-old-now';
}
if ($new_class) {
$classes[] = 'differential-update-history-new-now';
}
$rowc[] = nonempty(implode(' ', $classes), null);
}
Javelin::initBehavior(
'differential-diff-radios',
array(
'radios' => $radios,
));
$options = array(
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => pht('Ignore All'),
DifferentialChangesetParser::WHITESPACE_IGNORE_MOST => pht('Ignore Most'),
DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING =>
pht('Ignore Trailing'),
DifferentialChangesetParser::WHITESPACE_SHOW_ALL => pht('Show All'),
);
foreach ($options as $value => $label) {
$options[$value] = phutil_tag(
'option',
array(
'value' => $value,
'selected' => ($value == $this->selectedWhitespace)
? 'selected'
: null,
),
$label);
}
$select = phutil_tag('select', array('name' => 'whitespace'), $options);
$table = id(new AphrontTableView($rows));
$table->setHeaders(
array(
pht('Diff'),
pht('ID'),
pht('Base'),
pht('Description'),
pht('Created'),
pht('Lint'),
pht('Unit'),
'',
'',
));
$table->setColumnClasses(
array(
'pri',
'',
'',
'wide',
'date',
'center',
'center',
'center differential-update-history-old',
'center differential-update-history-new',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
true,
false,
true,
false,
false,
false,
true,
true,
));
$show_diff = phutil_tag(
'div',
array(
'class' => 'differential-update-history-footer',
),
array(
phutil_tag(
'label',
array(),
array(
pht('Whitespace Changes:'),
$select,
)),
phutil_tag(
'button',
array(),
pht('Show Diff')),
));
$content = phabricator_form(
$this->getUser(),
array(
- 'action' => '#toc',
+ 'action' => '/D'.$revision_id.'#toc',
),
array(
$table,
$show_diff,
));
return $content;
}
const STAR_NONE = 'none';
const STAR_OKAY = 'okay';
const STAR_WARN = 'warn';
const STAR_FAIL = 'fail';
const STAR_SKIP = 'skip';
private static function renderDiffLintStar(DifferentialDiff $diff) {
static $map = array(
DifferentialLintStatus::LINT_NONE => self::STAR_NONE,
DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY,
DifferentialLintStatus::LINT_WARN => self::STAR_WARN,
DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL,
DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP,
DifferentialLintStatus::LINT_AUTO_SKIP => self::STAR_SKIP,
);
$star = idx($map, $diff->getLintStatus(), self::STAR_FAIL);
return self::renderDiffStar($star);
}
private static function renderDiffUnitStar($unit_status) {
static $map = array(
DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE,
DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY,
DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN,
DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL,
DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP,
DifferentialUnitStatus::UNIT_AUTO_SKIP => self::STAR_SKIP,
);
$star = idx($map, $unit_status, self::STAR_FAIL);
return self::renderDiffStar($star);
}
public static function getDiffLintMessage(DifferentialDiff $diff) {
switch ($diff->getLintStatus()) {
case DifferentialLintStatus::LINT_NONE:
return pht('No Linters Available');
case DifferentialLintStatus::LINT_OKAY:
return pht('Lint OK');
case DifferentialLintStatus::LINT_WARN:
return pht('Lint Warnings');
case DifferentialLintStatus::LINT_FAIL:
return pht('Lint Errors');
case DifferentialLintStatus::LINT_SKIP:
return pht('Lint Skipped');
case DifferentialLintStatus::LINT_AUTO_SKIP:
return pht('Automatic diff as part of commit; lint not applicable.');
}
return pht('Unknown');
}
public static function getDiffUnitMessage($unit_status) {
switch ($unit_status) {
case DifferentialUnitStatus::UNIT_NONE:
return pht('No Unit Test Coverage');
case DifferentialUnitStatus::UNIT_OKAY:
return pht('Unit Tests OK');
case DifferentialUnitStatus::UNIT_WARN:
return pht('Unit Test Warnings');
case DifferentialUnitStatus::UNIT_FAIL:
return pht('Unit Test Errors');
case DifferentialUnitStatus::UNIT_SKIP:
return pht('Unit Tests Skipped');
case DifferentialUnitStatus::UNIT_AUTO_SKIP:
return pht(
'Automatic diff as part of commit; unit tests not applicable.');
}
return pht('Unknown');
}
private static function renderDiffStar($star) {
$class = 'diff-star-'.$star;
return phutil_tag(
'span',
array('class' => $class),
"\xE2\x98\x85");
}
private function renderBaseRevision(DifferentialDiff $diff) {
switch ($diff->getSourceControlSystem()) {
case 'git':
$base = $diff->getSourceControlBaseRevision();
if (strpos($base, '@') === false) {
$label = substr($base, 0, 7);
} else {
// The diff is from git-svn
$base = explode('@', $base);
$base = last($base);
$label = $base;
}
break;
case 'svn':
$base = $diff->getSourceControlBaseRevision();
$base = explode('@', $base);
$base = last($base);
$label = $base;
break;
default:
$label = null;
break;
}
$link = null;
if ($label) {
$commit_for_link = idx(
$this->commitsForLinks,
$diff->getSourceControlBaseRevision());
if ($commit_for_link) {
$link = phutil_tag(
'a',
array('href' => $commit_for_link->getURI()),
$label);
} else {
$link = $label;
}
}
return $link;
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php b/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php
index 6232594aa..c2baef034 100644
--- a/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php
@@ -1,89 +1,97 @@
<?php
final class DifferentialRevisionAbandonTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.abandon';
const ACTIONKEY = 'abandon';
protected function getRevisionActionLabel() {
return pht('Abandon Revision');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be abandoned and closed.');
}
public function getIcon() {
return 'fa-plane';
}
public function getColor() {
return 'indigo';
}
protected function getRevisionActionOrder() {
return 500;
}
public function getActionName() {
return pht('Abandoned');
}
public function getCommandKeyword() {
return 'abandon';
}
public function getCommandAliases() {
return array();
}
public function getCommandSummary() {
return pht('Abandon a revision.');
}
public function generateOldValue($object) {
return $object->isAbandoned();
}
public function applyInternalEffects($object, $value) {
$status_abandoned = DifferentialRevisionStatus::ABANDONED;
$object->setModernRevisionStatus($status_abandoned);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not abandon this revision because it has already been '.
'closed. Only open revisions can be abandoned.'));
}
$config_key = 'differential.always-allow-abandon';
if (!PhabricatorEnv::getEnvConfig($config_key)) {
if (!$this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not abandon this revision because you are not the '.
'author. You can only abandon revisions you own. You can change '.
'this behavior by adjusting the "%s" setting in Config.',
$config_key));
}
}
}
public function getTitle() {
return pht(
'%s abandoned this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s abandoned %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'abandon';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php
index 190a6a492..3d2df1489 100644
--- a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php
@@ -1,237 +1,245 @@
<?php
final class DifferentialRevisionAcceptTransaction
extends DifferentialRevisionReviewTransaction {
const TRANSACTIONTYPE = 'differential.revision.accept';
const ACTIONKEY = 'accept';
protected function getRevisionActionLabel() {
- return pht("Accept Revision \xE2\x9C\x94");
+ return pht('Accept Revision');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('These changes will be approved.');
}
public function getIcon() {
return 'fa-check-circle-o';
}
public function getColor() {
return 'green';
}
protected function getRevisionActionOrder() {
return 500;
}
public function getActionName() {
return pht('Accepted');
}
public function getCommandKeyword() {
$accept_key = 'differential.enable-email-accept';
$allow_email_accept = PhabricatorEnv::getEnvConfig($accept_key);
if (!$allow_email_accept) {
return null;
}
return 'accept';
}
public function getCommandAliases() {
return array();
}
public function getCommandSummary() {
return pht('Accept a revision.');
}
protected function getActionOptions(
PhabricatorUser $viewer,
DifferentialRevision $revision,
$include_accepted = false) {
$reviewers = $revision->getReviewers();
$options = array();
$value = array();
// Put the viewer's user reviewer first, if it exists, so that "Accept as
// yourself" is always at the top.
$head = array();
$tail = array();
foreach ($reviewers as $key => $reviewer) {
if ($reviewer->isUser()) {
$head[$key] = $reviewer;
} else {
$tail[$key] = $reviewer;
}
}
$reviewers = $head + $tail;
$diff_phid = $this->getActiveDiffPHID($revision);
$reviewer_phids = array();
// If the viewer isn't a reviewer, add them to the list of options first.
// This happens when you navigate to some revision you aren't involved in:
// you can accept and become a reviewer.
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
if (!isset($reviewers[$viewer_phid])) {
$reviewer_phids[$viewer_phid] = $viewer_phid;
}
}
$default_unchecked = array();
foreach ($reviewers as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
if (!$reviewer->hasAuthority($viewer)) {
// If the viewer doesn't have authority to act on behalf of a reviewer,
// we check if they can accept by force.
if ($revision->canReviewerForceAccept($viewer, $reviewer)) {
$default_unchecked[$reviewer_phid] = true;
} else {
continue;
}
}
if (!$include_accepted) {
if ($reviewer->isAccepted($diff_phid)) {
// If a reviewer is already in a full "accepted" state, don't
// include that reviewer as an option unless we're listing all
// reviewers, including reviewers who have already accepted.
continue;
}
}
$reviewer_phids[$reviewer_phid] = $reviewer_phid;
}
$handles = $viewer->loadHandles($reviewer_phids);
$head = array();
$tail = array();
foreach ($reviewer_phids as $reviewer_phid) {
$is_force = isset($default_unchecked[$reviewer_phid]);
if ($is_force) {
$tail[] = $reviewer_phid;
$options[$reviewer_phid] = pht(
'Force accept as %s',
$viewer->renderHandle($reviewer_phid));
} else {
$head[] = $reviewer_phid;
$value[] = $reviewer_phid;
$options[$reviewer_phid] = pht(
'Accept as %s',
$viewer->renderHandle($reviewer_phid));
}
}
// Reorder reviewers so "force accept" reviewers come at the end.
$options =
array_select_keys($options, $head) +
array_select_keys($options, $tail);
return array($options, $value);
}
public function generateOldValue($object) {
$actor = $this->getActor();
return $this->isViewerFullyAccepted($object, $actor);
}
public function applyExternalEffects($object, $value) {
$status = DifferentialReviewerStatus::STATUS_ACCEPTED;
$actor = $this->getActor();
$this->applyReviewerEffect($object, $actor, $value, $status);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not accept this revision because it has already been '.
'closed. Only open revisions can be accepted.'));
}
- if ($object->isDraft()) {
+ if ($object->isDraft() || !$object->getShouldBroadcast()) {
throw new Exception(
pht('You can not accept a draft revision.'));
}
$config_key = 'differential.allow-self-accept';
if (!PhabricatorEnv::getEnvConfig($config_key)) {
if ($this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not accept this revision because you are the revision '.
'author. You can only accept revisions you do not own. You can '.
'change this behavior by adjusting the "%s" setting in Config.',
$config_key));
}
}
if ($this->isViewerFullyAccepted($object, $viewer)) {
throw new Exception(
pht(
'You can not accept this revision because you have already '.
'accepted it.'));
}
}
protected function validateOptionValue($object, $actor, array $value) {
if (!$value) {
throw new Exception(
pht(
'When accepting a revision, you must accept on behalf of at '.
'least one reviewer.'));
}
// NOTE: We're including reviewers who have already been accepted in this
// check. Legitimate users may race one another to accept on behalf of
// packages. If we get a form submission which includes a reviewer which
// someone has already accepted, that's fine. See T12757.
list($options) = $this->getActionOptions($actor, $object, true);
foreach ($value as $phid) {
if (!isset($options[$phid])) {
throw new Exception(
pht(
'Reviewer "%s" is not a valid reviewer which you have authority '.
'to accept on behalf of.',
$phid));
}
}
}
public function getTitle() {
$new = $this->getNewValue();
if (is_array($new) && $new) {
return pht(
'%s accepted this revision as %s reviewer(s): %s.',
$this->renderAuthor(),
phutil_count($new),
$this->renderHandleList($new));
} else {
return pht(
'%s accepted this revision.',
$this->renderAuthor());
}
}
public function getTitleForFeed() {
return pht(
'%s accepted %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'accept';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php
index 7ef0f2ba6..f20ba7a01 100644
--- a/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php
@@ -1,158 +1,157 @@
<?php
final class DifferentialRevisionCloseTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.close';
const ACTIONKEY = 'close';
protected function getRevisionActionLabel() {
return pht('Close Revision');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be closed.');
}
public function getIcon() {
return 'fa-check';
}
public function getColor() {
return 'indigo';
}
protected function getRevisionActionOrder() {
return 300;
}
public function getActionName() {
return pht('Closed');
}
public function generateOldValue($object) {
return $object->isClosed();
}
public function applyInternalEffects($object, $value) {
$was_accepted = $object->isAccepted();
$status_published = DifferentialRevisionStatus::PUBLISHED;
$object->setModernRevisionStatus($status_published);
$object->setProperty(
DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED,
$was_accepted);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($this->hasEditor()) {
if ($this->getEditor()->getIsCloseByCommit()) {
// If we're closing a revision because we discovered a commit, we don't
// care what state it was in.
return;
}
}
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not close this revision because it has already been '.
'closed. Only open revisions can be closed.'));
}
if (!$object->isAccepted()) {
throw new Exception(
pht(
'You can not close this revision because it has not been accepted. '.
'Revisions must be accepted before they can be closed.'));
}
$config_key = 'differential.always-allow-close';
if (!PhabricatorEnv::getEnvConfig($config_key)) {
if (!$this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not close this revision because you are not the '.
'author. You can only close revisions you own. You can change '.
'this behavior by adjusting the "%s" setting in Config.',
$config_key));
}
}
}
public function getTitle() {
if (!$this->getMetadataValue('isCommitClose')) {
return pht(
'%s closed this revision.',
$this->renderAuthor());
}
$commit_phid = $this->getMetadataValue('commitPHID');
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($committer_phid) {
$committer_name = $this->renderHandle($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($author_phid) {
$author_name = $this->renderHandle($author_phid);
} else {
$author_name = $this->getMetadatavalue('authorName');
}
$same_phid =
strlen($committer_phid) &&
strlen($author_phid) &&
($committer_phid == $author_phid);
$same_name =
!strlen($committer_phid) &&
!strlen($author_phid) &&
($committer_name == $author_name);
if ($same_name || $same_phid) {
return pht(
'Closed by commit %s (authored by %s).',
$this->renderHandle($commit_phid),
$author_name);
} else {
return pht(
'Closed by commit %s (authored by %s, committed by %s).',
$this->renderHandle($commit_phid),
$author_name,
$committer_name);
}
}
public function getTitleForFeed() {
return pht(
'%s closed %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function getTransactionTypeForConduit($xaction) {
return 'close';
}
public function getFieldValuesForConduit($object, $data) {
$commit_phid = $object->getMetadataValue('commitPHID');
if ($commit_phid) {
$commit_phids = array($commit_phid);
} else {
$commit_phids = array();
}
return array(
'commitPHIDs' => $commit_phids,
);
}
-
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php
index 561e57c7c..5aafa536f 100644
--- a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php
@@ -1,90 +1,98 @@
<?php
final class DifferentialRevisionCommandeerTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.commandeer';
const ACTIONKEY = 'commandeer';
protected function getRevisionActionLabel() {
return pht('Commandeer Revision');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('You will take control of this revision and become its author.');
}
public function getIcon() {
return 'fa-flag';
}
public function getColor() {
return 'sky';
}
protected function getRevisionActionOrder() {
return 700;
}
public function getActionName() {
return pht('Commandeered');
}
public function getCommandKeyword() {
return 'commandeer';
}
public function getCommandAliases() {
return array(
'claim',
);
}
public function getCommandSummary() {
return pht('Commandeer a revision.');
}
public function generateOldValue($object) {
return $object->getAuthorPHID();
}
public function generateNewValue($object, $value) {
$actor = $this->getActor();
return $actor->getPHID();
}
public function applyInternalEffects($object, $value) {
$object->setAuthorPHID($value);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not commandeer this revision because it has already '.
'been closed. You can only commandeer open revisions.'));
}
if ($this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not commandeer this revision because you are already '.
'the author.'));
}
}
public function getTitle() {
return pht(
'%s commandeered this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s commandeered %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'commandeer';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php b/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php
index bdbe28d5c..f4fb0a3eb 100644
--- a/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php
@@ -1,122 +1,130 @@
<?php
final class DifferentialRevisionPlanChangesTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.plan';
const ACTIONKEY = 'plan-changes';
protected function getRevisionActionLabel() {
return pht('Plan Changes');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht(
'This revision will be removed from review queues until it is revised.');
}
public function getIcon() {
return 'fa-headphones';
}
public function getColor() {
return 'red';
}
protected function getRevisionActionOrder() {
return 200;
}
public function getActionName() {
return pht('Planned Changes');
}
public function getCommandKeyword() {
return 'planchanges';
}
public function getCommandAliases() {
return array(
'rethink',
);
}
public function getCommandSummary() {
return pht('Plan changes to a revision.');
}
public function generateOldValue($object) {
return $object->isChangePlanned();
}
public function applyInternalEffects($object, $value) {
$status_planned = DifferentialRevisionStatus::CHANGES_PLANNED;
$object->setModernRevisionStatus($status_planned);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isDraft()) {
// See PHI346. Until the "Draft" state fully unprototypes, allow drafts
// to be moved to "changes planned" via the API. This preserves the
// behavior of "arc diff --plan-changes". We still prevent this
// transition from the web UI.
// TODO: Remove this once drafts leave prototype.
$editor = $this->getEditor();
$type_web = PhabricatorWebContentSource::SOURCECONST;
if ($editor->getContentSource()->getSource() == $type_web) {
throw new Exception(
pht('You can not plan changes to a draft revision.'));
}
}
if ($object->isChangePlanned()) {
throw new Exception(
pht(
'You can not request review of this revision because this '.
'revision is already under review and the action would have '.
'no effect.'));
}
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not plan changes to this this revision because it has '.
'already been closed.'));
}
if (!$this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not plan changes to this revision because you do not '.
'own it. Only the author of a revision can plan changes to it.'));
}
}
public function getTitle() {
if ($this->isDraftDemotion()) {
return pht(
'%s returned this revision to the author for changes because remote '.
'builds failed.',
$this->renderAuthor());
} else {
return pht(
'%s planned changes to this revision.',
$this->renderAuthor());
}
}
public function getTitleForFeed() {
return pht(
'%s planned changes to %s.',
$this->renderAuthor(),
$this->renderObject());
}
private function isDraftDemotion() {
return (bool)$this->getMetadataValue('draft.demote');
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'plan-changes';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php
index 6d74589b6..e023dda0a 100644
--- a/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php
@@ -1,88 +1,96 @@
<?php
final class DifferentialRevisionReclaimTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.reclaim';
const ACTIONKEY = 'reclaim';
protected function getRevisionActionLabel() {
return pht('Reclaim Revision');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be reclaimed and reopened.');
}
public function getIcon() {
return 'fa-bullhorn';
}
public function getColor() {
return 'sky';
}
protected function getRevisionActionOrder() {
return 600;
}
public function getActionName() {
return pht('Reclaimed');
}
public function getCommandKeyword() {
return 'reclaim';
}
public function getCommandAliases() {
return array();
}
public function getCommandSummary() {
return pht('Reclaim a revision.');
}
public function generateOldValue($object) {
return !$object->isAbandoned();
}
public function applyInternalEffects($object, $value) {
if ($object->getShouldBroadcast()) {
$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
} else {
$new_status = DifferentialRevisionStatus::DRAFT;
}
$object->setModernRevisionStatus($new_status);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if (!$object->isAbandoned()) {
throw new Exception(
pht(
'You can not reclaim this revision because it has not been '.
'abandoned. Only abandoned revisions can be reclaimed.'));
}
if (!$this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not reclaim this revision because you are not the '.
'revision author. You can only reclaim revisions you own.'));
}
}
public function getTitle() {
return pht(
'%s reclaimed this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s reclaimed %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'reclaim';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php
index a15ca0641..0001ce09c 100644
--- a/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php
@@ -1,102 +1,110 @@
<?php
final class DifferentialRevisionRejectTransaction
extends DifferentialRevisionReviewTransaction {
const TRANSACTIONTYPE = 'differential.revision.reject';
const ACTIONKEY = 'reject';
protected function getRevisionActionLabel() {
- return pht("Request Changes \xE2\x9C\x98");
+ return pht('Request Changes');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be returned to the author for updates.');
}
public function getIcon() {
return 'fa-times-circle-o';
}
public function getColor() {
return 'red';
}
protected function getRevisionActionOrder() {
return 600;
}
public function getActionName() {
return pht('Requested Changes');
}
public function getCommandKeyword() {
return 'request';
}
public function getCommandAliases() {
return array(
'reject',
);
}
public function getCommandSummary() {
return pht('Request changes to a revision.');
}
public function generateOldValue($object) {
$actor = $this->getActor();
return $this->isViewerFullyRejected($object, $actor);
}
public function applyExternalEffects($object, $value) {
$status = DifferentialReviewerStatus::STATUS_REJECTED;
$actor = $this->getActor();
$this->applyReviewerEffect($object, $actor, $value, $status);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not request changes to this revision because it has '.
'already been closed. You can only request changes to open '.
'revisions.'));
}
if ($this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not request changes to this revision because you are the '.
'revision author. You can only request changes to revisions you do '.
'not own.'));
}
- if ($object->isDraft()) {
+ if ($object->isDraft() || !$object->getShouldBroadcast()) {
throw new Exception(
pht('You can not request changes to a draft revision.'));
}
if ($this->isViewerFullyRejected($object, $viewer)) {
throw new Exception(
pht(
'You can not request changes to this revision because you have '.
'already requested changes.'));
}
}
public function getTitle() {
return pht(
'%s requested changes to this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s requested changes to %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'request-changes';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php
index 4fc9f3a6f..a2d25287b 100644
--- a/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php
@@ -1,75 +1,83 @@
<?php
final class DifferentialRevisionReopenTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.reopen';
const ACTIONKEY = 'reopen';
protected function getRevisionActionLabel() {
return pht('Reopen Revision');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('This revision will be reopened for review.');
}
public function getIcon() {
return 'fa-bullhorn';
}
public function getColor() {
return 'sky';
}
protected function getRevisionActionOrder() {
return 400;
}
public function getActionName() {
return pht('Reopened');
}
public function generateOldValue($object) {
return !$object->isClosed();
}
public function applyInternalEffects($object, $value) {
$status_review = DifferentialRevisionStatus::NEEDS_REVIEW;
$object->setModernRevisionStatus($status_review);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if (!$object->isPublished()) {
throw new Exception(
pht(
'You can not reopen this revision because it is not closed. '.
'Only closed revisions can be reopened.'));
}
$config_key = 'differential.allow-reopen';
if (!PhabricatorEnv::getEnvConfig($config_key)) {
throw new Exception(
pht(
'You can not reopen this revision because configuration prevents '.
'any revision from being reopened. You can change this behavior '.
'by adjusting the "%s" setting in Config.',
$config_key));
}
}
public function getTitle() {
return pht(
'%s reopened this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s reopened %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'reopen';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php
index 08c590a9a..a3d2699c7 100644
--- a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php
@@ -1,87 +1,95 @@
<?php
final class DifferentialRevisionRequestReviewTransaction
extends DifferentialRevisionActionTransaction {
const TRANSACTIONTYPE = 'differential.revision.request';
const ACTIONKEY = 'request-review';
protected function getRevisionActionLabel() {
return pht('Request Review');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
if ($revision->isDraft()) {
return pht('This revision will be submitted to reviewers for feedback.');
} else {
return pht('This revision will be returned to reviewers for feedback.');
}
}
public function getColor() {
return 'sky';
}
protected function getRevisionActionOrder() {
return 200;
}
public function getActionName() {
return pht('Requested Review');
}
public function generateOldValue($object) {
return $object->isNeedsReview();
}
public function applyInternalEffects($object, $value) {
$status_review = DifferentialRevisionStatus::NEEDS_REVIEW;
$object
->setModernRevisionStatus($status_review)
->setShouldBroadcast(true);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isNeedsReview()) {
throw new Exception(
pht(
'You can not request review of this revision because this '.
'revision is already under review and the action would have '.
'no effect.'));
}
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not request review of this revision because it has '.
'already been closed. You can only request review of open '.
'revisions.'));
}
// When revisions automatically promote out of "Draft" after builds finish,
// the viewer may be acting as the Harbormaster application.
if (!$viewer->isOmnipotent()) {
if (!$this->isViewerRevisionAuthor($object, $viewer)) {
throw new Exception(
pht(
'You can not request review of this revision because you are not '.
'the author of the revision.'));
}
}
}
public function getTitle() {
return pht(
'%s requested review of this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s requested review of %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'request-review';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php b/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php
index 53b0b8da0..a2b4fd433 100644
--- a/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php
@@ -1,96 +1,104 @@
<?php
final class DifferentialRevisionResignTransaction
extends DifferentialRevisionReviewTransaction {
const TRANSACTIONTYPE = 'differential.revision.resign';
const ACTIONKEY = 'resign';
protected function getRevisionActionLabel() {
return pht('Resign as Reviewer');
}
protected function getRevisionActionDescription(
DifferentialRevision $revision) {
return pht('You will resign as a reviewer for this change.');
}
public function getIcon() {
return 'fa-flag';
}
public function getColor() {
return 'orange';
}
protected function getRevisionActionOrder() {
return 700;
}
public function getCommandKeyword() {
return 'resign';
}
public function getActionName() {
return pht('Resigned');
}
public function getCommandAliases() {
return array();
}
public function getCommandSummary() {
return pht('Resign from a revision.');
}
public function generateOldValue($object) {
$actor = $this->getActor();
$resigned = DifferentialReviewerStatus::STATUS_RESIGNED;
return ($this->getViewerReviewerStatus($object, $actor) == $resigned);
}
public function applyExternalEffects($object, $value) {
$status = DifferentialReviewerStatus::STATUS_RESIGNED;
$actor = $this->getActor();
$this->applyReviewerEffect($object, $actor, $value, $status);
}
protected function validateAction($object, PhabricatorUser $viewer) {
if ($object->isClosed()) {
throw new Exception(
pht(
'You can not resign from this revision because it has already '.
'been closed. You can only resign from open revisions.'));
}
$resigned = DifferentialReviewerStatus::STATUS_RESIGNED;
if ($this->getViewerReviewerStatus($object, $viewer) == $resigned) {
throw new Exception(
pht(
'You can not resign from this revision because you have already '.
'resigned.'));
}
if (!$this->isViewerAnyAuthority($object, $viewer)) {
throw new Exception(
pht(
'You can not resign from this revision because you are not a '.
'reviewer, and do not have authority over any reviewer.'));
}
}
public function getTitle() {
return pht(
'%s resigned from this revision.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s resigned from %s.',
$this->renderAuthor(),
$this->renderObject());
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'resign';
+ }
+
+ public function getFieldValuesForConduit($object, $data) {
+ return array();
+ }
+
}
diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
index f4af3df0c..bb6b52c52 100644
--- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
+++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php
@@ -1,198 +1,207 @@
<?php
final class PhabricatorDiffusionApplication extends PhabricatorApplication {
public function getName() {
return pht('Diffusion');
}
public function getShortDescription() {
return pht('Host and Browse Repositories');
}
public function getBaseURI() {
return '/diffusion/';
}
public function getIcon() {
return 'fa-code';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Diffusion User Guide'),
'href' => PhabricatorEnv::getDoclink('Diffusion User Guide'),
),
array(
'name' => pht('Audit User Guide'),
'href' => PhabricatorEnv::getDoclink('Audit User Guide'),
),
);
}
public function getRemarkupRules() {
return array(
new DiffusionCommitRemarkupRule(),
new DiffusionRepositoryRemarkupRule(),
new DiffusionRepositoryByIDRemarkupRule(),
);
}
public function getRoutes() {
$repository_routes = array(
'/' => array(
'' => 'DiffusionRepositoryController',
'repository/(?P<dblob>.*)' => 'DiffusionRepositoryController',
'change/(?P<dblob>.*)' => 'DiffusionChangeController',
'clone/' => 'DiffusionCloneController',
'history/(?P<dblob>.*)' => 'DiffusionHistoryController',
'graph/(?P<dblob>.*)' => 'DiffusionGraphController',
'jobs/(?P<dblob>.*)' => 'DiffusionJenkinsJobController', // c4s custo
'browse/(?P<dblob>.*)' => 'DiffusionBrowseController',
'document/(?P<dblob>.*)'
=> 'DiffusionDocumentController',
'blame/(?P<dblob>.*)'
=> 'DiffusionBlameController',
'lastmodified/(?P<dblob>.*)' => 'DiffusionLastModifiedController',
'diff/' => 'DiffusionDiffController',
'tags/(?P<dblob>.*)' => 'DiffusionTagListController',
'branches/(?P<dblob>.*)' => 'DiffusionBranchTableController',
'refs/(?P<dblob>.*)' => 'DiffusionRefTableController',
'lint/(?P<dblob>.*)' => 'DiffusionLintController',
'commit/(?P<commit>[a-z0-9]+)' => array(
'/?' => 'DiffusionCommitController',
'/branches/' => 'DiffusionCommitBranchesController',
'/tags/' => 'DiffusionCommitTagsController',
),
'compare/' => 'DiffusionCompareController',
'manage/(?:(?P<panel>[^/]+)/)?'
=> 'DiffusionRepositoryManagePanelsController',
'uri/' => array(
'view/(?P<id>[0-9]\d*)/' => 'DiffusionRepositoryURIViewController',
'disable/(?P<id>[0-9]\d*)/'
=> 'DiffusionRepositoryURIDisableController',
$this->getEditRoutePattern('edit/')
=> 'DiffusionRepositoryURIEditController',
'credential/(?P<id>[0-9]\d*)/(?P<action>edit|remove)/'
=> 'DiffusionRepositoryURICredentialController',
),
'edit/' => array(
'activate/' => 'DiffusionRepositoryEditActivateController',
'dangerous/' => 'DiffusionRepositoryEditDangerousController',
'enormous/' => 'DiffusionRepositoryEditEnormousController',
'delete/' => 'DiffusionRepositoryEditDeleteController',
'update/' => 'DiffusionRepositoryEditUpdateController',
'testautomation/' => 'DiffusionRepositoryTestAutomationController',
),
'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
),
// NOTE: This must come after the rules above; it just gives us a
// catch-all for serving repositories over HTTP. We must accept requests
// without the trailing "/" because SVN commands don't necessarily
// include it.
'(?:/.*)?' => 'DiffusionRepositoryDefaultController',
);
return array(
'/(?:'.
'r(?P<repositoryCallsign>[A-Z]+)'.
'|'.
'R(?P<repositoryID>[1-9]\d*):'.
')(?P<commit>[a-f0-9]+)'
=> 'DiffusionCommitController',
'/source/(?P<repositoryShortName>[^/]+)'
=> $repository_routes,
'/diffusion/' => array(
$this->getQueryRoutePattern()
=> 'DiffusionRepositoryListController',
$this->getEditRoutePattern('edit/') =>
'DiffusionRepositoryEditController',
'pushlog/' => array(
$this->getQueryRoutePattern() => 'DiffusionPushLogListController',
'view/(?P<id>\d+)/' => 'DiffusionPushEventViewController',
),
'pulllog/' => array(
$this->getQueryRoutePattern() => 'DiffusionPullLogListController',
),
'(?P<repositoryCallsign>[A-Z]+)' => $repository_routes,
'(?P<repositoryID>[1-9]\d*)' => $repository_routes,
+ 'identity/' => array(
+ $this->getQueryRoutePattern() =>
+ 'DiffusionIdentityListController',
+ $this->getEditRoutePattern('edit/') =>
+ 'DiffusionIdentityEditController',
+ 'view/(?P<id>[^/]+)/' =>
+ 'DiffusionIdentityViewController',
+ ),
+
'inline/' => array(
'edit/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController',
'preview/(?P<phid>[^/]+)/'
=> 'DiffusionInlineCommentPreviewController',
),
'services/' => array(
'path/' => array(
'complete/' => 'DiffusionPathCompleteController',
'validate/' => 'DiffusionPathValidateController',
),
),
'symbol/(?P<name>[^/]+)/' => 'DiffusionSymbolController',
'external/' => 'DiffusionExternalController',
'lint/' => 'DiffusionLintController',
'commit/' => array(
$this->getQueryRoutePattern() =>
'DiffusionCommitListController',
$this->getEditRoutePattern('edit/') =>
'DiffusionCommitEditController',
),
'picture/(?P<id>[0-9]\d*)/'
=> 'DiffusionRepositoryProfilePictureController',
),
);
}
public function getApplicationOrder() {
return 0.120;
}
protected function getCustomCapabilities() {
return array(
DiffusionDefaultViewCapability::CAPABILITY => array(
'template' => PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
DiffusionDefaultEditCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
'template' => PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
),
DiffusionDefaultPushCapability::CAPABILITY => array(
'template' => PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
),
DiffusionCreateRepositoriesCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
public function getMailCommandObjects() {
return array(
'commit' => array(
'name' => pht('Email Commands: Commits'),
'header' => pht('Interacting with Commits'),
'object' => new PhabricatorRepositoryCommit(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'commits and audits in Diffusion.'),
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorRepositoryCommitPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php
index d42c45a96..62878c029 100644
--- a/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php
@@ -1,128 +1,141 @@
<?php
final class DiffusionBranchQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.branchquery';
}
public function getMethodDescription() {
return pht('Determine what branches exist for a repository.');
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineCustomParamTypes() {
return array(
'closed' => 'optional bool',
'limit' => 'optional int',
'offset' => 'optional int',
'contains' => 'optional string',
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$contains = $request->getValue('contains');
if (strlen($contains)) {
+
+ // See PHI720. If the standard "branch" field is provided, use it
+ // as the "pattern" argument to "git branch ..." to let callers test
+ // for reachability from a particular branch head.
+ $pattern = $request->getValue('branch');
+ if (strlen($pattern)) {
+ $pattern_argv = array($pattern);
+ } else {
+ $pattern_argv = array();
+ }
+
// NOTE: We can't use DiffusionLowLevelGitRefQuery here because
// `git for-each-ref` does not support `--contains`.
if ($repository->isWorkingCopyBare()) {
list($stdout) = $repository->execxLocalCommand(
- 'branch --verbose --no-abbrev --contains %s --',
- $contains);
+ 'branch --verbose --no-abbrev --contains %s -- %Ls',
+ $contains,
+ $pattern_argv);
$ref_map = DiffusionGitBranch::parseLocalBranchOutput(
$stdout);
} else {
list($stdout) = $repository->execxLocalCommand(
- 'branch -r --verbose --no-abbrev --contains %s --',
- $contains);
+ 'branch -r --verbose --no-abbrev --contains %s -- %Ls',
+ $contains,
+ $pattern_argv);
$ref_map = DiffusionGitBranch::parseRemoteBranchOutput(
$stdout,
DiffusionGitBranch::DEFAULT_GIT_REMOTE);
}
$refs = array();
foreach ($ref_map as $ref => $commit) {
$refs[] = id(new DiffusionRepositoryRef())
->setShortName($ref)
->setCommitIdentifier($commit);
}
} else {
$refs = id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->withRefTypes(
array(
PhabricatorRepositoryRefCursor::TYPE_BRANCH,
))
->execute();
}
return $this->processBranchRefs($request, $refs);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$query = id(new DiffusionLowLevelMercurialBranchesQuery())
->setRepository($repository);
$contains = $request->getValue('contains');
if (strlen($contains)) {
$query->withContainsCommit($contains);
}
$refs = $query->execute();
return $this->processBranchRefs($request, $refs);
}
protected function getSVNResult(ConduitAPIRequest $request) {
// Since SVN doesn't have meaningful branches, just return nothing for all
// queries.
return array();
}
private function processBranchRefs(ConduitAPIRequest $request, array $refs) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
foreach ($refs as $key => $ref) {
if (!$repository->shouldTrackBranch($ref->getShortName())) {
unset($refs[$key]);
}
}
$with_closed = $request->getValue('closed');
if ($with_closed !== null) {
foreach ($refs as $key => $ref) {
$fields = $ref->getRawFields();
if (idx($fields, 'closed') != $with_closed) {
unset($refs[$key]);
}
}
}
// NOTE: We can't apply the offset or limit until here, because we may have
// filtered untrackable branches out of the result set.
if ($offset) {
$refs = array_slice($refs, $offset);
}
if ($limit) {
$refs = array_slice($refs, 0, $limit);
}
return mpull($refs, 'toDictionary');
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php
index 002805ac4..286831f82 100644
--- a/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php
@@ -1,66 +1,84 @@
<?php
final class DiffusionGetRecentCommitsByPathConduitAPIMethod
extends DiffusionConduitAPIMethod {
const DEFAULT_LIMIT = 10;
public function getAPIMethodName() {
return 'diffusion.getrecentcommitsbypath';
}
+ public function getMethodStatus() {
+ return self::METHOD_STATUS_DEPRECATED;
+ }
+
+ public function getMethodStatusDescription() {
+ return pht('Obsoleted by "diffusion.historyquery".');
+ }
+
public function getMethodDescription() {
return pht(
'Get commit identifiers for recent commits affecting a given path.');
}
protected function defineParamTypes() {
return array(
'callsign' => 'required string',
'path' => 'required string',
'branch' => 'optional string',
'limit' => 'optional int',
);
}
+ protected function defineErrorTypes() {
+ return array(
+ 'ERR_NOT_FOUND' => pht('Repository was not found.'),
+ );
+ }
+
protected function defineReturnType() {
return 'nonempty list<string>';
}
protected function execute(ConduitAPIRequest $request) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $request->getUser(),
'callsign' => $request->getValue('callsign'),
'path' => $request->getValue('path'),
'branch' => $request->getValue('branch'),
));
+ if ($drequest === null) {
+ throw new ConduitException('ERR_NOT_FOUND');
+ }
+
$limit = nonempty(
$request->getValue('limit'),
self::DEFAULT_LIMIT);
$history_result = DiffusionQuery::callConduitWithDiffusionRequest(
$request->getUser(),
$drequest,
'diffusion.historyquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'offset' => 0,
'limit' => $limit,
'needDirectChanges' => true,
'needChildChanges' => true,
));
$history = DiffusionPathChange::newFromConduit(
$history_result['pathChanges']);
$raw_commit_identifiers = mpull($history, 'getCommitIdentifier');
$result = array();
foreach ($raw_commit_identifiers as $id) {
$result[] = 'r'.$request->getValue('callsign').$id;
}
return $result;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php
new file mode 100644
index 000000000..01808bb4e
--- /dev/null
+++ b/src/applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php
@@ -0,0 +1,51 @@
+<?php
+
+final class DiffusionInternalAncestorsConduitAPIMethod
+ extends DiffusionQueryConduitAPIMethod {
+
+ public function isInternalAPI() {
+ return true;
+ }
+
+ public function getAPIMethodName() {
+ return 'diffusion.internal.ancestors';
+ }
+
+ public function getMethodDescription() {
+ return pht('Internal method for filtering ref ancestors.');
+ }
+
+ protected function defineReturnType() {
+ return 'list<string>';
+ }
+
+ protected function defineCustomParamTypes() {
+ return array(
+ 'ref' => 'required string',
+ 'commits' => 'required list<string>',
+ );
+ }
+
+ protected function getResult(ConduitAPIRequest $request) {
+ $drequest = $this->getDiffusionRequest();
+ $repository = $drequest->getRepository();
+
+ $commits = $request->getValue('commits');
+ $ref = $request->getValue('ref');
+
+ $graph = new PhabricatorGitGraphStream($repository, $ref);
+
+ $keep = array();
+ foreach ($commits as $identifier) {
+ try {
+ $graph->getCommitDate($identifier);
+ $keep[] = $identifier;
+ } catch (Exception $ex) {
+ // Not an ancestor.
+ }
+ }
+
+ return $keep;
+ }
+
+}
diff --git a/src/applications/diffusion/controller/DiffusionBlameController.php b/src/applications/diffusion/controller/DiffusionBlameController.php
index 66403a8af..42cabbc0d 100644
--- a/src/applications/diffusion/controller/DiffusionBlameController.php
+++ b/src/applications/diffusion/controller/DiffusionBlameController.php
@@ -1,266 +1,284 @@
<?php
final class DiffusionBlameController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$blame = $this->loadBlame();
$identifiers = array_fuse($blame);
if ($identifiers) {
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers($identifiers)
+ ->needIdentities(true)
->execute();
$commits = mpull($commits, null, 'getCommitIdentifier');
} else {
$commits = array();
}
$commit_map = mpull($commits, 'getCommitIdentifier', 'getPHID');
$revisions = array();
$revision_map = array();
if ($commits) {
$revision_ids = id(new DifferentialRevision())
->loadIDsByCommitPHIDs(array_keys($commit_map));
if ($revision_ids) {
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs($revision_ids)
->execute();
$revisions = mpull($revisions, null, 'getID');
}
foreach ($revision_ids as $commit_phid => $revision_id) {
// If the viewer can't actually see this revision, skip it.
if (!isset($revisions[$revision_id])) {
continue;
}
$revision_map[$commit_map[$commit_phid]] = $revision_id;
}
}
$base_href = (string)$drequest->generateURI(
array(
'action' => 'browse',
'stable' => true,
));
$skip_text = pht('Skip Past This Commit');
$skip_icon = id(new PHUIIconView())
->setIcon('fa-backward');
Javelin::initBehavior('phabricator-tooltips');
$handle_phids = array();
foreach ($commits as $commit) {
- $author_phid = $commit->getAuthorPHID();
- if ($author_phid) {
- $handle_phids[] = $author_phid;
- }
+ $handle_phids[] = $commit->getAuthorDisplayPHID();
}
foreach ($revisions as $revision) {
$handle_phids[] = $revision->getAuthorPHID();
}
$handles = $viewer->loadHandles($handle_phids);
-
$map = array();
$epochs = array();
foreach ($identifiers as $identifier) {
$revision_id = idx($revision_map, $identifier);
if ($revision_id) {
$revision = idx($revisions, $revision_id);
} else {
$revision = null;
}
$skip_href = $base_href.'?before='.$identifier;
$skip_link = javelin_tag(
'a',
array(
'href' => $skip_href,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $skip_text,
'align' => 'E',
'size' => 300,
),
),
$skip_icon);
- $commit = $commits[$identifier];
+ // We may not have a commit object for a given identifier if the commit
+ // has not imported yet.
+
+ // At time of writing, this can also happen if a line was part of the
+ // initial import: blame produces a "^abc123" identifier in Git, which
+ // doesn't correspond to a real commit.
+
+ $commit = idx($commits, $identifier);
+
+ $author_phid = null;
+
+ if ($commit) {
+ $author_phid = $commit->getAuthorDisplayPHID();
+ }
- $author_phid = $commit->getAuthorPHID();
if (!$author_phid && $revision) {
$author_phid = $revision->getAuthorPHID();
}
if (!$author_phid) {
// This means we couldn't identify an author for the commit or the
// revision. We just render a blank for alignment.
$author_style = null;
$author_href = null;
$author_sigil = null;
$author_meta = null;
} else {
$author_src = $handles[$author_phid]->getImageURI();
$author_style = 'background-image: url('.$author_src.');';
$author_href = $handles[$author_phid]->getURI();
$author_sigil = 'has-tooltip';
$author_meta = array(
'tip' => $handles[$author_phid]->getName(),
'align' => 'E',
);
}
$author_link = javelin_tag(
$author_href ? 'a' : 'span',
array(
'class' => 'phabricator-source-blame-author',
'style' => $author_style,
'href' => $author_href,
'sigil' => $author_sigil,
'meta' => $author_meta,
));
- $commit_link = javelin_tag(
- 'a',
- array(
- 'href' => $commit->getURI(),
- 'sigil' => 'has-tooltip',
- 'meta' => array(
- 'tip' => $this->renderCommitTooltip($commit, $handles),
- 'align' => 'E',
- 'size' => 600,
+ if ($commit) {
+ $commit_link = javelin_tag(
+ 'a',
+ array(
+ 'href' => $commit->getURI(),
+ 'sigil' => 'has-tooltip',
+ 'meta' => array(
+ 'tip' => $this->renderCommitTooltip($commit, $handles),
+ 'align' => 'E',
+ 'size' => 600,
+ ),
),
- ),
- $commit->getLocalName());
+ $commit->getLocalName());
+ } else {
+ $commit_link = null;
+ }
$info = array(
$author_link,
$commit_link,
);
if ($revision) {
$revision_link = javelin_tag(
'a',
array(
'href' => $revision->getURI(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $this->renderRevisionTooltip($revision, $handles),
'align' => 'E',
'size' => 600,
),
),
$revision->getMonogram());
$info = array(
$info,
" \xC2\xB7 ",
$revision_link,
);
}
- $epoch = $commit->getEpoch();
+ if ($commit) {
+ $epoch = $commit->getEpoch();
+ } else {
+ $epoch = 0;
+ }
+
$epochs[] = $epoch;
$data = array(
'skip' => $skip_link,
'info' => hsprintf('%s', $info),
'epoch' => $epoch,
);
$map[$identifier] = $data;
}
$epoch_min = min($epochs);
$epoch_max = max($epochs);
return id(new AphrontAjaxResponse())->setContent(
array(
'blame' => $blame,
'map' => $map,
'epoch' => array(
'min' => $epoch_min,
'max' => $epoch_max,
),
));
}
private function loadBlame() {
$drequest = $this->getDiffusionRequest();
$commit = $drequest->getCommit();
$path = $drequest->getPath();
$blame_timeout = 15;
$blame = $this->callConduitWithDiffusionRequest(
'diffusion.blame',
array(
'commit' => $commit,
'paths' => array($path),
'timeout' => $blame_timeout,
));
return idx($blame, $path, array());
}
private function renderRevisionTooltip(
DifferentialRevision $revision,
$handles) {
$viewer = $this->getViewer();
$date = phabricator_date($revision->getDateModified(), $viewer);
$monogram = $revision->getMonogram();
$title = $revision->getTitle();
$header = "{$monogram} {$title}";
$author = $handles[$revision->getAuthorPHID()]->getName();
return "{$header}\n{$date} \xC2\xB7 {$author}";
}
private function renderCommitTooltip(
PhabricatorRepositoryCommit $commit,
$handles) {
$viewer = $this->getViewer();
$date = phabricator_date($commit->getEpoch(), $viewer);
$summary = trim($commit->getSummary());
$author_phid = $commit->getAuthorPHID();
if ($author_phid && isset($handles[$author_phid])) {
$author_name = $handles[$author_phid]->getName();
} else {
$author_name = null;
}
if ($author_name) {
return "{$summary}\n{$date} \xC2\xB7 {$author_name}";
} else {
return "{$summary}\n{$date}";
}
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php
index 441421ba3..65d7b8fd1 100644
--- a/src/applications/diffusion/controller/DiffusionBranchTableController.php
+++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php
@@ -1,99 +1,100 @@
<?php
final class DiffusionBranchTableController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$pager = id(new PHUIPagerView())
->readFromRequest($request);
$params = array(
'offset' => $pager->getOffset(),
'limit' => $pager->getPageSize() + 1,
+ 'branch' => null,
);
$contains = $drequest->getSymbolicCommit();
if (strlen($contains)) {
$params['contains'] = $contains;
}
$branches = $this->callConduitWithDiffusionRequest(
'diffusion.branchquery',
$params);
$branches = $pager->sliceResults($branches);
$branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches);
$content = null;
if (!$branches) {
$content = $this->renderStatusMessage(
pht('No Branches'),
pht('This repository has no branches.'));
} else {
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIdentifiers(mpull($branches, 'getCommitIdentifier'))
->withRepository($repository)
->execute();
$list = id(new DiffusionBranchListView())
->setUser($viewer)
->setBranches($branches)
->setCommits($commits)
->setDiffusionRequest($drequest);
$content = id(new PHUIObjectBoxView())
->setHeaderText($repository->getName())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setTable($list)
->setPager($pager);
}
$crumbs = $this->buildCrumbs(
array(
'branches' => true,
));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Branches'))
->setHeaderIcon('fa-code-fork');
if (!$repository->isSVN()) {
$branch_tag = $this->renderBranchTag($drequest);
$header->addTag($branch_tag);
}
$tabs = $this->buildTabsView('branch');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(array(
$content,
));
return $this->newPage()
->setTitle(
array(
pht('Branches'),
$repository->getDisplayName(),
))
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php
index 1053ef477..54be7dd7f 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseController.php
@@ -1,1127 +1,1125 @@
<?php
final class DiffusionBrowseController extends DiffusionController {
private $lintCommit;
private $lintMessages;
- private $coverage;
private $corpusButtons = array();
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$drequest = $this->getDiffusionRequest();
// Figure out if we're browsing a directory, a file, or a search result
// list.
$grep = $request->getStr('grep');
if (strlen($grep)) {
return $this->browseSearch();
}
$pager = id(new PHUIPagerView())
->readFromRequest($request);
$results = DiffusionBrowseResultSet::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.browsequery',
array(
'path' => $drequest->getPath(),
'commit' => $drequest->getStableCommit(),
'offset' => $pager->getOffset(),
'limit' => $pager->getPageSize() + 1,
)));
$reason = $results->getReasonForEmptyResultSet();
$is_file = ($reason == DiffusionBrowseResultSet::REASON_IS_FILE);
if ($is_file) {
return $this->browseFile();
}
$paths = $results->getPaths();
$paths = $pager->sliceResults($paths);
$results->setPaths($paths);
return $this->browseDirectory($results, $pager);
}
private function browseSearch() {
$drequest = $this->getDiffusionRequest();
$header = $this->buildHeaderView($drequest);
$path = nonempty(basename($drequest->getPath()), '/');
$search_results = $this->renderSearchResults();
$search_form = $this->renderSearchForm($path);
$search_form = phutil_tag(
'div',
array(
'class' => 'diffusion-mobile-search-form',
),
$search_form);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$crumbs->setBorder(true);
$tabs = $this->buildTabsView('code');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(
array(
$search_form,
$search_results,
));
return $this->newPage()
->setTitle(
array(
nonempty(basename($drequest->getPath()), '/'),
$drequest->getRepository()->getDisplayName(),
))
->setCrumbs($crumbs)
->appendChild($view);
}
private function browseFile() {
$viewer = $this->getViewer();
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$before = $request->getStr('before');
if ($before) {
return $this->buildBeforeResponse($before);
}
$path = $drequest->getPath();
$params = array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
);
$view = $request->getStr('view');
$byte_limit = null;
if ($view !== 'raw') {
$byte_limit = PhabricatorFileStorageEngine::getChunkThreshold();
$time_limit = 10;
$params += array(
'timeout' => $time_limit,
'byteLimit' => $byte_limit,
);
}
$response = $this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
$params);
$hit_byte_limit = $response['tooHuge'];
$hit_time_limit = $response['tooSlow'];
$file_phid = $response['filePHID'];
$show_editor = false;
if ($hit_byte_limit) {
$corpus = $this->buildErrorCorpus(
pht(
'This file is larger than %s byte(s), and too large to display '.
'in the web UI.',
phutil_format_bytes($byte_limit)));
} else if ($hit_time_limit) {
$corpus = $this->buildErrorCorpus(
pht(
'This file took too long to load from the repository (more than '.
'%s second(s)).',
new PhutilNumber($time_limit)));
} else {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(pht('Failed to load content file!'));
}
if ($view === 'raw') {
return $file->getRedirectResponse();
}
$data = $file->loadFileData();
$lfs_ref = $this->getGitLFSRef($repository, $data);
if ($lfs_ref) {
if ($view == 'git-lfs') {
$file = $this->loadGitLFSFile($lfs_ref);
// Rename the file locally so we generate a better vanity URI for
// it. In storage, it just has a name like "lfs-13f9a94c0923...",
// since we don't get any hints about possible human-readable names
// at upload time.
$basename = basename($drequest->getPath());
$file->makeEphemeral();
$file->setName($basename);
return $file->getRedirectResponse();
}
$corpus = $this->buildGitLFSCorpus($lfs_ref);
} else {
- $this->coverage = $drequest->loadCoverage();
$show_editor = true;
$ref = id(new PhabricatorDocumentRef())
->setFile($file);
$engine = id(new DiffusionDocumentRenderingEngine())
->setRequest($request)
->setDiffusionRequest($drequest);
$corpus = $engine->newDocumentView($ref);
$this->corpusButtons[] = $this->renderFileButton();
}
}
$bar = $this->buildButtonBar($drequest, $show_editor);
$header = $this->buildHeaderView($drequest);
$header->setHeaderIcon('fa-file-code-o');
$follow = $request->getStr('follow');
$follow_notice = null;
if ($follow) {
$follow_notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Unable to Continue'));
switch ($follow) {
case 'first':
$follow_notice->appendChild(
pht(
'Unable to continue tracing the history of this file because '.
'this commit is the first commit in the repository.'));
break;
case 'created':
$follow_notice->appendChild(
pht(
'Unable to continue tracing the history of this file because '.
'this commit created the file.'));
break;
}
}
$renamed = $request->getStr('renamed');
$renamed_notice = null;
if ($renamed) {
$renamed_notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('File Renamed'))
->appendChild(
pht(
'File history passes through a rename from "%s" to "%s".',
$drequest->getPath(),
$renamed));
}
$open_revisions = $this->buildOpenRevisions();
$owners_list = $this->buildOwnersList($drequest);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$crumbs->setBorder(true);
$basename = basename($this->getDiffusionRequest()->getPath());
$tabs = $this->buildTabsView('code');
$bar->setRight($this->corpusButtons);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(array(
$bar,
$follow_notice,
$renamed_notice,
$corpus,
$open_revisions,
$owners_list,
));
$title = array($basename, $repository->getDisplayName());
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
public function browseDirectory(
DiffusionBrowseResultSet $results,
PHUIPagerView $pager) {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$reason = $results->getReasonForEmptyResultSet();
$this->buildActionButtons($drequest, true);
$details = $this->buildPropertyView($drequest);
$header = $this->buildHeaderView($drequest);
$header->setHeaderIcon('fa-folder-open');
$empty_result = null;
$browse_panel = null;
$branch_panel = null;
if (!$results->isValidResults()) {
$empty_result = new DiffusionEmptyResultView();
$empty_result->setDiffusionRequest($drequest);
$empty_result->setDiffusionBrowseResultSet($results);
$empty_result->setView($request->getStr('view'));
} else {
$phids = array();
foreach ($results->getPaths() as $result) {
$data = $result->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$browse_table = id(new DiffusionBrowseTableView())
->setDiffusionRequest($drequest)
->setHandles($handles)
->setPaths($results->getPaths())
->setUser($request->getUser());
$title = nonempty(basename($drequest->getPath()), '/');
$icon = 'fa-folder-open';
$browse_header = $this->buildPanelHeaderView($title, $icon);
$browse_panel = id(new PHUIObjectBoxView())
->setHeader($browse_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($browse_table)
->addClass('diffusion-mobile-view')
->setPager($pager);
$path = $drequest->getPath();
$is_branch = (!strlen($path) && $repository->supportsBranchComparison());
if ($is_branch) {
$branch_panel = $this->buildBranchTable();
}
}
$open_revisions = $this->buildOpenRevisions();
$readme = $this->renderDirectoryReadme($results);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$crumbs->setBorder(true);
$tabs = $this->buildTabsView('code');
$owners_list = $this->buildOwnersList($drequest);
$bar = id(new PHUILeftRightView())
->setRight($this->corpusButtons)
->addClass('diffusion-action-bar');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(
array(
$bar,
$branch_panel,
$empty_result,
$browse_panel,
$open_revisions,
$owners_list,
$readme,
));
if ($details) {
$view->addPropertySection(pht('Details'), $details);
}
return $this->newPage()
->setTitle(array(
nonempty(basename($drequest->getPath()), '/'),
$repository->getDisplayName(),
))
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function renderSearchResults() {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$results = array();
$pager = id(new PHUIPagerView())
->readFromRequest($request);
$search_mode = null;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$results = array();
break;
default:
if (strlen($this->getRequest()->getStr('grep'))) {
$search_mode = 'grep';
$query_string = $request->getStr('grep');
$results = $this->callConduitWithDiffusionRequest(
'diffusion.searchquery',
array(
'grep' => $query_string,
'commit' => $drequest->getStableCommit(),
'path' => $drequest->getPath(),
'limit' => $pager->getPageSize() + 1,
'offset' => $pager->getOffset(),
));
}
break;
}
$results = $pager->sliceResults($results);
$table = null;
$header = null;
if ($search_mode == 'grep') {
$table = $this->renderGrepResults($results, $query_string);
$title = pht(
'File content matching "%s" under "%s"',
$query_string,
nonempty($drequest->getPath(), '/'));
$header = id(new PHUIHeaderView())
->setHeader($title)
->addClass('diffusion-search-result-header');
}
return array($header, $table, $pager);
}
private function renderGrepResults(array $results, $pattern) {
$drequest = $this->getDiffusionRequest();
require_celerity_resource('phabricator-search-results-css');
if (!$results) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild(
pht(
'The pattern you searched for was not found in the content of any '.
'files.'));
}
$grouped = array();
foreach ($results as $file) {
list($path, $line, $string) = $file;
$grouped[$path][] = array($line, $string);
}
$view = array();
foreach ($grouped as $path => $matches) {
$view[] = id(new DiffusionPatternSearchView())
->setPath($path)
->setMatches($matches)
->setPattern($pattern)
->setDiffusionRequest($drequest)
->render();
}
return $view;
}
private function buildButtonBar(
DiffusionRequest $drequest,
$show_editor) {
$viewer = $this->getViewer();
$base_uri = $this->getRequest()->getRequestURI();
$user = $this->getRequest()->getUser();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$line = nonempty((int)$drequest->getLine(), 1);
$buttons = array();
$editor_link = $user->loadEditorLink($path, $line, $repository);
$template = $user->loadEditorLink($path, '%l', $repository);
$buttons[] =
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Last Change'))
->setColor(PHUIButtonView::GREY)
->setHref(
$drequest->generateURI(
array(
'action' => 'change',
)))
->setIcon('fa-backward');
if ($editor_link) {
$buttons[] =
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Open File'))
->setHref($editor_link)
->setIcon('fa-pencil')
->setID('editor_link')
->setMetadata(array('link_template' => $template))
->setDisabled(!$editor_link)
->setColor(PHUIButtonView::GREY);
}
$bar = id(new PHUILeftRightView())
->setLeft($buttons)
->addClass('diffusion-action-bar full-mobile-buttons');
return $bar;
}
private function buildOwnersList(DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$have_owners = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorOwnersApplication',
$viewer);
if (!$have_owners) {
return null;
}
$repository = $drequest->getRepository();
$package_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withControl(
$repository->getPHID(),
array(
$drequest->getPath(),
));
$package_query->execute();
$packages = $package_query->getControllingPackagesForPath(
$repository->getPHID(),
$drequest->getPath());
$ownership = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('No Owners'));
if ($packages) {
foreach ($packages as $package) {
$item = id(new PHUIObjectItemView())
->setObject($package)
->setObjectName($package->getMonogram())
->setHeader($package->getName())
->setHref($package->getURI());
$owners = $package->getOwners();
if ($owners) {
$owner_list = $viewer->renderHandleList(
mpull($owners, 'getUserPHID'));
} else {
$owner_list = phutil_tag('em', array(), pht('None'));
}
$item->addAttribute(pht('Owners: %s', $owner_list));
$auto = $package->getAutoReview();
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$spec = idx($autoreview_map, $auto, array());
$name = idx($spec, 'name', $auto);
$item->addIcon('fa-code', $name);
if ($package->getAuditingEnabled()) {
$item->addIcon('fa-check', pht('Auditing Enabled'));
} else {
$item->addIcon('fa-ban', pht('No Auditing'));
}
if ($package->isArchived()) {
$item->setDisabled(true);
}
$ownership->addItem($item);
}
}
$view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Owner Packages'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setObjectList($ownership);
return $view;
}
private function renderFileButton($file_uri = null, $label = null) {
$base_uri = $this->getRequest()->getRequestURI();
if ($file_uri) {
$text = pht('Download File');
$href = $file_uri;
$icon = 'fa-download';
} else {
$text = pht('Raw File');
$href = $base_uri->alter('view', 'raw');
$icon = 'fa-file-text';
}
if ($label !== null) {
$text = $label;
}
$button = id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($href)
->setIcon($icon)
->setColor(PHUIButtonView::GREY);
return $button;
}
private function renderGitLFSButton() {
$viewer = $this->getViewer();
$uri = $this->getRequest()->getRequestURI();
$href = $uri->alter('view', 'git-lfs');
$text = pht('Download from Git LFS');
$icon = 'fa-download';
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($href)
->setIcon($icon)
->setColor(PHUIButtonView::GREY);
}
private function buildErrorCorpus($message) {
$text = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->appendChild($message);
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($text);
return $box;
}
private function buildBeforeResponse($before) {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
// NOTE: We need to get the grandparent so we can capture filename changes
// in the parent.
$parent = $this->loadParentCommitOf($before);
$old_filename = null;
$was_created = false;
if ($parent) {
$grandparent = $this->loadParentCommitOf($parent);
if ($grandparent) {
$rename_query = new DiffusionRenameHistoryQuery();
$rename_query->setRequest($drequest);
$rename_query->setOldCommit($grandparent);
$rename_query->setViewer($request->getUser());
$old_filename = $rename_query->loadOldFilename();
$was_created = $rename_query->getWasCreated();
}
}
$follow = null;
if ($was_created) {
// If the file was created in history, that means older commits won't
// have it. Since we know it existed at 'before', it must have been
// created then; jump there.
$target_commit = $before;
$follow = 'created';
} else if ($parent) {
// If we found a parent, jump to it. This is the normal case.
$target_commit = $parent;
} else {
// If there's no parent, this was probably created in the initial commit?
// And the "was_created" check will fail because we can't identify the
// grandparent. Keep the user at 'before'.
$target_commit = $before;
$follow = 'first';
}
$path = $drequest->getPath();
$renamed = null;
if ($old_filename !== null &&
$old_filename !== '/'.$path) {
$renamed = $path;
$path = $old_filename;
}
$line = null;
// If there's a follow error, drop the line so the user sees the message.
if (!$follow) {
$line = $this->getBeforeLineNumber($target_commit);
}
$before_uri = $drequest->generateURI(
array(
'action' => 'browse',
'commit' => $target_commit,
'line' => $line,
'path' => $path,
));
$before_uri->setQueryParams($request->getRequestURI()->getQueryParams());
$before_uri = $before_uri->alter('before', null);
$before_uri = $before_uri->alter('renamed', $renamed);
$before_uri = $before_uri->alter('follow', $follow);
return id(new AphrontRedirectResponse())->setURI($before_uri);
}
private function getBeforeLineNumber($target_commit) {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
$line = $drequest->getLine();
if (!$line) {
return null;
}
$diff_info = $this->callConduitWithDiffusionRequest(
'diffusion.rawdiffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'againstCommit' => $target_commit,
));
$file_phid = $diff_info['filePHID'];
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery.'));
}
$raw_diff = $file->loadFileData();
$old_line = 0;
$new_line = 0;
foreach (explode("\n", $raw_diff) as $text) {
if ($text[0] == '-' || $text[0] == ' ') {
$old_line++;
}
if ($text[0] == '+' || $text[0] == ' ') {
$new_line++;
}
if ($new_line == $line) {
return $old_line;
}
}
// We didn't find the target line.
return $line;
}
private function loadParentCommitOf($commit) {
$drequest = $this->getDiffusionRequest();
$user = $this->getRequest()->getUser();
$before_req = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $drequest->getRepository(),
'commit' => $commit,
));
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$before_req,
'diffusion.commitparentsquery',
array(
'commit' => $commit,
));
return head($parents);
}
protected function markupText($text) {
$engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
$engine->setConfig('viewer', $this->getRequest()->getUser());
$text = $engine->markupText($text);
$text = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$text);
return $text;
}
protected function buildHeaderView(DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$repository = $drequest->getRepository();
$commit_tag = $this->renderCommitHashTag($drequest);
$path = nonempty($drequest->getPath(), '/');
$search = $this->renderSearchForm($path);
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($this->renderPathLinks($drequest, $mode = 'browse'))
->addActionItem($search)
->addTag($commit_tag)
->addClass('diffusion-browse-header');
if (!$repository->isSVN()) {
$branch_tag = $this->renderBranchTag($drequest);
$header->addTag($branch_tag);
}
return $header;
}
protected function buildPanelHeaderView($title, $icon) {
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon($icon)
->addClass('diffusion-panel-header-view');
return $header;
}
protected function buildActionButtons(
DiffusionRequest $drequest,
$is_directory = false) {
$viewer = $this->getViewer();
$repository = $drequest->getRepository();
$history_uri = $drequest->generateURI(array('action' => 'history'));
$behind_head = $drequest->getSymbolicCommit();
$compare = null;
$head_uri = $drequest->generateURI(
array(
'commit' => '',
'action' => 'browse',
));
if ($repository->supportsBranchComparison() && $is_directory) {
$compare_uri = $drequest->generateURI(array('action' => 'compare'));
$compare = id(new PHUIButtonView())
->setText(pht('Compare'))
->setIcon('fa-code-fork')
->setWorkflow(true)
->setTag('a')
->setHref($compare_uri)
->setColor(PHUIButtonView::GREY);
$this->corpusButtons[] = $compare;
}
$head = null;
if ($behind_head) {
$head = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Back to HEAD'))
->setHref($head_uri)
->setIcon('fa-home')
->setColor(PHUIButtonView::GREY);
$this->corpusButtons[] = $head;
}
$history = id(new PHUIButtonView())
->setText(pht('History'))
->setHref($history_uri)
->setTag('a')
->setIcon('fa-history')
->setColor(PHUIButtonView::GREY);
$this->corpusButtons[] = $history;
}
protected function buildPropertyView(
DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
if ($drequest->getSymbolicType() == 'tag') {
$symbolic = $drequest->getSymbolicCommit();
$view->addProperty(pht('Tag'), $symbolic);
$tags = $this->callConduitWithDiffusionRequest(
'diffusion.tagsquery',
array(
'names' => array($symbolic),
'needMessages' => true,
));
$tags = DiffusionRepositoryTag::newFromConduit($tags);
$tags = mpull($tags, null, 'getName');
$tag = idx($tags, $symbolic);
if ($tag && strlen($tag->getMessage())) {
$view->addSectionHeader(
pht('Tag Content'), 'fa-tag');
$view->addTextContent($this->markupText($tag->getMessage()));
}
}
if ($view->hasAnyProperties()) {
return $view;
}
return null;
}
private function buildOpenRevisions() {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
$path_id = idx($path_map, $path);
if (!$path_id) {
return null;
}
$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withPath($repository->getID(), $path_id)
->withIsOpen(true)
->withUpdatedEpochBetween($recent, null)
->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
->setLimit(10)
->needReviewers(true)
->needFlags(true)
->needDrafts(true)
->execute();
if (!$revisions) {
return null;
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Recently Open Revisions'));
$list = id(new DifferentialRevisionListView())
->setViewer($viewer)
->setRevisions($revisions)
->setNoBox(true);
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->appendChild($list);
return $view;
}
private function getGitLFSRef(PhabricatorRepository $repository, $data) {
if (!$repository->canUseGitLFS()) {
return null;
}
$lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])';
if (!preg_match($lfs_pattern, $data)) {
return null;
}
$matches = null;
if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) {
return null;
}
$hash = $matches[1];
$hash = trim($hash);
return id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($this->getViewer())
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes(array($hash))
->executeOne();
}
private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef $ref) {
// TODO: We should probably test if we can load the file PHID here and
// show the user an error if we can't, rather than making them click
// through to hit an error.
$title = basename($this->getDiffusionRequest()->getPath());
$icon = 'fa-archive';
$drequest = $this->getDiffusionRequest();
$this->buildActionButtons($drequest);
$header = $this->buildPanelHeaderView($title, $icon);
$severity = PHUIInfoView::SEVERITY_NOTICE;
$messages = array();
$messages[] = pht(
'This %s file is stored in Git Large File Storage.',
phutil_format_bytes($ref->getByteSize()));
try {
$file = $this->loadGitLFSFile($ref);
$this->corpusButtons[] = $this->renderGitLFSButton();
} catch (Exception $ex) {
$severity = PHUIInfoView::SEVERITY_ERROR;
$messages[] = pht('The data for this file could not be loaded.');
}
$this->corpusButtons[] = $this->renderFileButton(
null, pht('View Raw LFS Pointer'));
$corpus = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setCollapsed(true);
if ($messages) {
$corpus->setInfoView(
id(new PHUIInfoView())
->setSeverity($severity)
->setErrors($messages));
}
return $corpus;
}
private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef $ref) {
$viewer = $this->getViewer();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($ref->getFilePHID()))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load file object for Git LFS ref "%s"!',
$ref->getObjectHash()));
}
return $file;
}
private function buildBranchTable() {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$branch = $drequest->getBranch();
$default_branch = $repository->getDefaultBranch();
if ($branch === $default_branch) {
return null;
}
$pager = id(new PHUIPagerView())
->setPageSize(10);
try {
$results = $this->callConduitWithDiffusionRequest(
'diffusion.historyquery',
array(
'commit' => $branch,
'against' => $default_branch,
'path' => $drequest->getPath(),
'offset' => $pager->getOffset(),
'limit' => $pager->getPageSize() + 1,
));
} catch (Exception $ex) {
return null;
}
$history = DiffusionPathChange::newFromConduit($results['pathChanges']);
$history = $pager->sliceResults($history);
if (!$history) {
return null;
}
$history_table = id(new DiffusionHistoryTableView())
->setViewer($viewer)
->setDiffusionRequest($drequest)
->setHistory($history);
$history_table->loadRevisions();
$history_table
->setParents($results['parents'])
->setFilterParents(true)
->setIsHead(true)
->setIsTail(!$pager->getHasMorePages());
$header = id(new PHUIHeaderView())
->setHeader(pht('%s vs %s', $branch, $default_branch));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setTable($history_table);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
index e698ff6fc..633c3b917 100644
--- a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
@@ -1,59 +1,60 @@
<?php
final class DiffusionCommitBranchesController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$branch_limit = 10;
$branches = DiffusionRepositoryRef::loadAllFromDictionaries(
$this->callConduitWithDiffusionRequest(
'diffusion.branchquery',
array(
'contains' => $drequest->getCommit(),
'limit' => $branch_limit + 1,
+ 'branch' => null,
)));
$has_more_branches = (count($branches) > $branch_limit);
$branches = array_slice($branches, 0, $branch_limit);
$branch_links = array();
foreach ($branches as $branch) {
$branch_links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'browse',
'branch' => $branch->getShortName(),
)),
),
$branch->getShortName());
}
if ($has_more_branches) {
$branch_links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'branches',
)),
),
pht("More Branches\xE2\x80\xA6"));
}
return id(new AphrontAjaxResponse())
->setContent($branch_links ? implode(', ', $branch_links) : pht('None'));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index 67ffb7335..a6743b655 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1,1171 +1,1155 @@
<?php
final class DiffusionCommitController extends DiffusionController {
const CHANGES_LIMIT = 100;
private $commitParents;
private $commitRefs;
private $commitMerges;
private $commitErrors;
private $commitExists;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$drequest = $this->getDiffusionRequest();
$viewer = $request->getUser();
$repository = $drequest->getRepository();
$commit_identifier = $drequest->getCommit();
// If this page is being accessed via "/source/xyz/commit/...", redirect
// to the canonical URI.
$has_callsign = strlen($request->getURIData('repositoryCallsign'));
$has_id = strlen($request->getURIData('repositoryID'));
if (!$has_callsign && !$has_id) {
$canonical_uri = $repository->getCommitURI($commit_identifier);
return id(new AphrontRedirectResponse())
->setURI($canonical_uri);
}
if ($request->getStr('diff')) {
return $this->buildRawDiffResponse($drequest);
}
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers(array($commit_identifier))
->needCommitData(true)
->needAuditRequests(true)
->setLimit(100)
+ ->needIdentities(true)
->execute();
$multiple_results = count($commits) > 1;
$crumbs = $this->buildCrumbs(array(
'commit' => !$multiple_results,
));
$crumbs->setBorder(true);
if (!$commits) {
if (!$this->getCommitExists()) {
return new Aphront404Response();
}
$error = id(new PHUIInfoView())
->setTitle(pht('Commit Still Parsing'))
->appendChild(
pht(
'Failed to load the commit because the commit has not been '.
'parsed yet.'));
$title = pht('Commit Still Parsing');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($error);
} else if ($multiple_results) {
$warning_message =
pht(
'The identifier %s is ambiguous and matches more than one commit.',
phutil_tag(
'strong',
array(),
$commit_identifier));
$error = id(new PHUIInfoView())
->setTitle(pht('Ambiguous Commit'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($warning_message);
$list = id(new DiffusionCommitListView())
->setViewer($viewer)
->setCommits($commits)
->setNoDataString(pht('No recent commits.'));
$crumbs->addTextCrumb(pht('Ambiguous Commit'));
$matched_commits = id(new PHUITwoColumnView())
->setFooter(array(
$error,
$list,
));
return $this->newPage()
->setTitle(pht('Ambiguous Commit'))
->setCrumbs($crumbs)
->appendChild($matched_commits);
} else {
$commit = head($commits);
}
$audit_requests = $commit->getAudits();
$commit->loadAndAttachAuditAuthority($viewer);
$commit_data = $commit->getCommitData();
$is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
$error_panel = null;
$hard_limit = 1000;
if ($commit->isImported()) {
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$change_query->setLimit($hard_limit + 1);
$changes = $change_query->loadChanges();
} else {
$changes = array();
}
$was_limited = (count($changes) > $hard_limit);
if ($was_limited) {
$changes = array_slice($changes, 0, $hard_limit);
}
$count = count($changes);
$is_unreadable = false;
$hint = null;
if (!$count || $commit->isUnreachable()) {
$hint = id(new DiffusionCommitHintQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withOldCommitIdentifiers(array($commit->getCommitIdentifier()))
->executeOne();
if ($hint) {
$is_unreadable = $hint->isUnreadable();
}
}
if ($is_foreign) {
$subpath = $commit_data->getCommitDetail('svn-subpath');
$error_panel = new PHUIInfoView();
$error_panel->setTitle(pht('Commit Not Tracked'));
$error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING);
$error_panel->appendChild(
pht(
"This Diffusion repository is configured to track only one ".
"subdirectory of the entire Subversion repository, and this commit ".
"didn't affect the tracked subdirectory ('%s'), so no ".
"information is available.",
$subpath));
} else {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
$commit_tag = $this->renderCommitHashTag($drequest);
$header = id(new PHUIHeaderView())
->setHeader(nonempty($commit->getSummary(), pht('Commit Detail')))
->setHeaderIcon('fa-code-fork')
->addTag($commit_tag);
if ($commit->getAuditStatus()) {
$icon = PhabricatorAuditCommitStatusConstants::getStatusIcon(
$commit->getAuditStatus());
$color = PhabricatorAuditCommitStatusConstants::getStatusColor(
$commit->getAuditStatus());
$status = PhabricatorAuditCommitStatusConstants::getStatusName(
$commit->getAuditStatus());
$header->setStatus($icon, $color, $status);
}
$curtain = $this->buildCurtain($commit, $repository);
$subheader = $this->buildSubheaderView($commit, $commit_data);
$details = $this->buildPropertyListView(
$commit,
$commit_data,
$audit_requests);
$message = $commit_data->getCommitMessage();
$revision = $commit->getCommitIdentifier();
$message = $this->linkBugtraq($message);
$message = $engine->markupText($message);
$detail_list = new PHUIPropertyListView();
$detail_list->addTextContent(
phutil_tag(
'div',
array(
'class' => 'diffusion-commit-message phabricator-remarkup',
),
$message));
if ($commit->isUnreachable()) {
$did_rewrite = false;
if ($hint) {
if ($hint->isRewritten()) {
$rewritten = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers(array($hint->getNewCommitIdentifier()))
->executeOne();
if ($rewritten) {
$did_rewrite = true;
$rewritten_uri = $rewritten->getURI();
$rewritten_name = $rewritten->getLocalName();
$rewritten_link = phutil_tag(
'a',
array(
'href' => $rewritten_uri,
),
$rewritten_name);
$this->commitErrors[] = pht(
'This commit was rewritten after it was published, which '.
'changed the commit hash. This old version of the commit is '.
'no longer reachable from any branch, tag or ref. The new '.
'version of this commit is %s.',
$rewritten_link);
}
}
}
if (!$did_rewrite) {
$this->commitErrors[] = pht(
'This commit has been deleted in the repository: it is no longer '.
'reachable from any branch, tag, or ref.');
}
}
if ($this->getCommitErrors()) {
$error_panel = id(new PHUIInfoView())
->appendChild($this->getCommitErrors())
->setSeverity(PHUIInfoView::SEVERITY_WARNING);
}
}
$timeline = $this->buildComments($commit);
$merge_table = $this->buildMergesTable($commit);
$show_changesets = false;
$info_panel = null;
$change_list = null;
$change_table = null;
if ($is_unreadable) {
$info_panel = $this->renderStatusMessage(
pht('Unreadable Commit'),
pht(
'This commit has been marked as unreadable by an administrator. '.
'It may have been corrupted or created improperly by an external '.
'tool.'));
} else if ($is_foreign) {
// Don't render anything else.
} else if (!$commit->isImported()) {
$info_panel = $this->renderStatusMessage(
pht('Still Importing...'),
pht(
'This commit is still importing. Changes will be visible once '.
'the import finishes.'));
} else if (!count($changes)) {
$info_panel = $this->renderStatusMessage(
pht('Empty Commit'),
pht(
'This commit is empty and does not affect any paths.'));
} else if ($was_limited) {
$info_panel = $this->renderStatusMessage(
pht('Very Large Commit'),
pht(
'This commit is very large, and affects more than %d files. '.
'Changes are not shown.',
$hard_limit));
} else if (!$this->getCommitExists()) {
$info_panel = $this->renderStatusMessage(
pht('Commit No Longer Exists'),
pht('This commit no longer exists in the repository.'));
} else {
$show_changesets = true;
// The user has clicked "Show All Changes", and we should show all the
// changes inline even if there are more than the soft limit.
$show_all_details = $request->getBool('show_all');
$change_header = id(new PHUIHeaderView())
->setHeader(pht('Changes (%s)', new PhutilNumber($count)));
$warning_view = null;
if ($count > self::CHANGES_LIMIT && !$show_all_details) {
$button = id(new PHUIButtonView())
->setText(pht('Show All Changes'))
->setHref('?show_all=true')
->setTag('a')
->setIcon('fa-files-o');
$warning_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Very Large Commit'))
->appendChild(
pht('This commit is very large. Load each file individually.'));
$change_header->addActionLink($button);
}
$changesets = DiffusionPathChange::convertToDifferentialChangesets(
$viewer,
$changes);
// TODO: This table and panel shouldn't really be separate, but we need
// to clean up the "Load All Files" interaction first.
$change_table = $this->buildTableOfContents(
$changesets,
$change_header,
$warning_view);
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$vcs_supports_directory_changes = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$vcs_supports_directory_changes = false;
break;
default:
throw new Exception(pht('Unknown VCS.'));
}
$references = array();
foreach ($changesets as $key => $changeset) {
$file_type = $changeset->getFileType();
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
if (!$vcs_supports_directory_changes) {
unset($changesets[$key]);
continue;
}
}
$references[$key] = $drequest->generateURI(
array(
'action' => 'rendering-ref',
'path' => $changeset->getFilename(),
));
}
// TODO: Some parts of the views still rely on properties of the
// DifferentialChangeset. Make the objects ephemeral to make sure we don't
// accidentally save them, and then set their ID to the appropriate ID for
// this application (the path IDs).
$path_ids = array_flip(mpull($changes, 'getPath'));
foreach ($changesets as $changeset) {
$changeset->makeEphemeral();
$changeset->setID($path_ids[$changeset->getFilename()]);
}
if ($count <= self::CHANGES_LIMIT || $show_all_details) {
$visible_changesets = $changesets;
} else {
$visible_changesets = array();
$inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments(
$viewer,
$commit->getPHID());
$path_ids = mpull($inlines, null, 'getPathID');
foreach ($changesets as $key => $changeset) {
if (array_key_exists($changeset->getID(), $path_ids)) {
$visible_changesets[$key] = $changeset;
}
}
}
$change_list_title = $commit->getDisplayName();
$change_list = new DifferentialChangesetListView();
$change_list->setTitle($change_list_title);
$change_list->setChangesets($changesets);
$change_list->setVisibleChangesets($visible_changesets);
$change_list->setRenderingReferences($references);
$change_list->setRenderURI($repository->getPathURI('diff/'));
$change_list->setRepository($repository);
$change_list->setUser($viewer);
$change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
// TODO: Try to setBranch() to something reasonable here?
$change_list->setStandaloneURI(
$repository->getPathURI('diff/'));
$change_list->setRawFileURIs(
// TODO: Implement this, somewhat tricky if there's an octopus merge
// or whatever?
null,
$repository->getPathURI('diff/?view=r'));
$change_list->setInlineCommentControllerURI(
'/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/');
}
$add_comment = $this->renderAddCommentPanel(
$commit,
$timeline);
$filetree_on = $viewer->compareUserSetting(
PhabricatorShowFiletreeSetting::SETTINGKEY,
PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE);
$nav = null;
if ($show_changesets && $filetree_on) {
$pref_collapse = PhabricatorFiletreeVisibleSetting::SETTINGKEY;
$collapsed = $viewer->getUserSetting($pref_collapse);
$pref_width = PhabricatorFiletreeWidthSetting::SETTINGKEY;
$width = $viewer->getUserSetting($pref_width);
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle($commit->getDisplayName())
->setBaseURI(new PhutilURI($commit->getURI()))
->build($changesets)
->setCrumbs($crumbs)
->setCollapsed((bool)$collapsed)
->setWidth((int)$width);
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setMainColumn(array(
$error_panel,
$timeline,
$merge_table,
$info_panel,
))
->setFooter(array(
$change_table,
$change_list,
$add_comment,
))
->addPropertySection(pht('Description'), $detail_list)
->addPropertySection(pht('Details'), $details)
->setCurtain($curtain);
$page = $this->newPage()
->setTitle($commit->getDisplayName())
->setCrumbs($crumbs)
->setPageObjectPHIDS(array($commit->getPHID()))
->appendChild(
array(
$view,
));
if ($nav) {
$page->setNavigation($nav);
}
return $page;
}
private function buildPropertyListView(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data,
array $audit_requests) {
$viewer = $this->getViewer();
$commit_phid = $commit->getPHID();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$view = id(new PHUIPropertyListView())
->setUser($this->getRequest()->getUser())
->setObject($commit);
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($commit_phid))
->withEdgeTypes(array(
DiffusionCommitHasTaskEdgeType::EDGECONST,
DiffusionCommitHasRevisionEdgeType::EDGECONST,
DiffusionCommitRevertsCommitEdgeType::EDGECONST,
DiffusionCommitRevertedByCommitEdgeType::EDGECONST,
));
$edges = $edge_query->execute();
$task_phids = array_keys(
$edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]);
$revision_phid = key(
$edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]);
$reverts_phids = array_keys(
$edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]);
$reverted_by_phids = array_keys(
$edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]);
$phids = $edge_query->getDestinationPHIDs(array($commit_phid));
- if ($data->getCommitDetail('authorPHID')) {
- $phids[] = $data->getCommitDetail('authorPHID');
- }
+
if ($data->getCommitDetail('reviewerPHID')) {
$phids[] = $data->getCommitDetail('reviewerPHID');
}
- if ($data->getCommitDetail('committerPHID')) {
- $phids[] = $data->getCommitDetail('committerPHID');
- }
+
+ $phids[] = $commit->getCommitterDisplayPHID();
+ $phids[] = $commit->getAuthorDisplayPHID();
// NOTE: We should never normally have more than a single push log, but
// it can occur naturally if a commit is pushed, then the branch it was
// on is deleted, then the commit is pushed again (or through other similar
// chains of events). This should be rare, but does not indicate a bug
// or data issue.
// NOTE: We never query push logs in SVN because the committer is always
// the pusher and the commit time is always the push time; the push log
// is redundant and we save a query by skipping it.
$push_logs = array();
if ($repository->isHosted() && !$repository->isSVN()) {
$push_logs = id(new PhabricatorRepositoryPushLogQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withNewRefs(array($commit->getCommitIdentifier()))
->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT))
->execute();
foreach ($push_logs as $log) {
$phids[] = $log->getPusherPHID();
}
}
$handles = array();
if ($phids) {
$handles = $this->loadViewerHandles($phids);
}
$props = array();
if ($audit_requests) {
$user_requests = array();
$other_requests = array();
foreach ($audit_requests as $audit_request) {
if (!$audit_request->isInteresting()) {
continue;
}
if ($audit_request->isUser()) {
$user_requests[] = $audit_request;
} else {
$other_requests[] = $audit_request;
}
}
if ($user_requests) {
$view->addProperty(
pht('Auditors'),
$this->renderAuditStatusView($commit, $user_requests));
}
if ($other_requests) {
$view->addProperty(
pht('Group Auditors'),
$this->renderAuditStatusView($commit, $other_requests));
}
}
- $author_phid = $data->getCommitDetail('authorPHID');
- $author_name = $data->getAuthorName();
$author_epoch = $data->getCommitDetail('authorEpoch');
$committed_info = id(new PHUIStatusItemView())
- ->setNote(phabricator_datetime($commit->getEpoch(), $viewer));
-
- $committer_phid = $data->getCommitDetail('committerPHID');
- $committer_name = $data->getCommitDetail('committer');
- if ($committer_phid) {
- $committed_info->setTarget($handles[$committer_phid]->renderLink());
- } else if (strlen($committer_name)) {
- $committed_info->setTarget($committer_name);
- } else if ($author_phid) {
- $committed_info->setTarget($handles[$author_phid]->renderLink());
- } else if (strlen($author_name)) {
- $committed_info->setTarget($author_name);
- }
+ ->setNote(phabricator_datetime($commit->getEpoch(), $viewer))
+ ->setTarget($commit->renderAnyCommitter($viewer, $handles));
$committed_list = new PHUIStatusListView();
$committed_list->addItem($committed_info);
$view->addProperty(
pht('Committed'),
$committed_list);
if ($push_logs) {
$pushed_list = new PHUIStatusListView();
foreach ($push_logs as $push_log) {
$pushed_item = id(new PHUIStatusItemView())
->setTarget($handles[$push_log->getPusherPHID()]->renderLink())
->setNote(phabricator_datetime($push_log->getEpoch(), $viewer));
$pushed_list->addItem($pushed_item);
}
$view->addProperty(
pht('Pushed'),
$pushed_list);
}
$reviewer_phid = $data->getCommitDetail('reviewerPHID');
if ($reviewer_phid) {
$view->addProperty(
pht('Reviewer'),
$handles[$reviewer_phid]->renderLink());
}
if ($revision_phid) {
$view->addProperty(
pht('Differential Revision'),
$handles[$revision_phid]->renderLink());
}
$parents = $this->getCommitParents();
if ($parents) {
$view->addProperty(
pht('Parents'),
$viewer->renderHandleList(mpull($parents, 'getPHID')));
}
if ($this->getCommitExists()) {
$view->addProperty(
pht('Branches'),
phutil_tag(
'span',
array(
'id' => 'commit-branches',
),
pht('Unknown')));
$view->addProperty(
pht('Tags'),
phutil_tag(
'span',
array(
'id' => 'commit-tags',
),
pht('Unknown')));
$identifier = $commit->getCommitIdentifier();
$root = $repository->getPathURI("commit/{$identifier}");
Javelin::initBehavior(
'diffusion-commit-branches',
array(
$root.'/branches/' => 'commit-branches',
$root.'/tags/' => 'commit-tags',
));
}
$refs = $this->getCommitRefs();
if ($refs) {
$ref_links = array();
foreach ($refs as $ref_data) {
$ref_links[] = phutil_tag(
'a',
array(
'href' => $ref_data['href'],
),
$ref_data['ref']);
}
$view->addProperty(
pht('References'),
phutil_implode_html(', ', $ref_links));
}
if ($reverts_phids) {
$view->addProperty(
pht('Reverts'),
$viewer->renderHandleList($reverts_phids));
}
if ($reverted_by_phids) {
$view->addProperty(
pht('Reverted By'),
$viewer->renderHandleList($reverted_by_phids));
}
if ($task_phids) {
$task_list = array();
foreach ($task_phids as $phid) {
$task_list[] = $handles[$phid]->renderLink();
}
$task_list = phutil_implode_html(phutil_tag('br'), $task_list);
$view->addProperty(
pht('Tasks'),
$task_list);
}
return $view;
}
private function buildSubheaderView(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
if ($repository->isSVN()) {
return null;
}
- $author_phid = $data->getCommitDetail('authorPHID');
+ $author_phid = $commit->getAuthorDisplayPHID();
$author_name = $data->getAuthorName();
$author_epoch = $data->getCommitDetail('authorEpoch');
$date = null;
if ($author_epoch !== null) {
$date = phabricator_datetime($author_epoch, $viewer);
}
if ($author_phid) {
$handles = $viewer->loadHandles(array($author_phid));
$image_uri = $handles[$author_phid]->getImageURI();
$image_href = $handles[$author_phid]->getURI();
$author = $handles[$author_phid]->renderLink();
} else if (strlen($author_name)) {
$author = $author_name;
$image_uri = null;
$image_href = null;
} else {
return null;
}
$author = phutil_tag('strong', array(), $author);
if ($date) {
$content = pht('Authored by %s on %s.', $author, $date);
} else {
$content = pht('Authored by %s.', $author);
}
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
-
}
-
private function buildComments(PhabricatorRepositoryCommit $commit) {
$timeline = $this->buildTransactionTimeline(
$commit,
new PhabricatorAuditTransactionQuery());
$commit->willRenderTimeline($timeline, $this->getRequest());
$timeline->setQuoteRef($commit->getMonogram());
return $timeline;
}
private function renderAddCommentPanel(
PhabricatorRepositoryCommit $commit,
$timeline) {
$request = $this->getRequest();
$viewer = $request->getUser();
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
$comment_view = id(new DiffusionCommitEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($commit);
$comment_view->setTransactionTimeline($timeline);
return $comment_view;
}
private function buildMergesTable(PhabricatorRepositoryCommit $commit) {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$merges = $this->getCommitMerges();
if (!$merges) {
return null;
}
$limit = $this->getMergeDisplayLimit();
$caption = null;
if (count($merges) > $limit) {
$merges = array_slice($merges, 0, $limit);
$caption = new PHUIInfoView();
$caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$caption->appendChild(
pht(
'This commit merges a very large number of changes. '.
'Only the first %s are shown.',
new PhutilNumber($limit)));
}
$history_table = id(new DiffusionHistoryTableView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setHistory($merges);
$history_table->loadRevisions();
$panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Merged Changes'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($history_table);
if ($caption) {
$panel->setInfoView($caption);
}
return $panel;
}
private function buildCurtain(
PhabricatorRepositoryCommit $commit,
PhabricatorRepository $repository) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($commit);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$commit,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $commit->getID();
$edit_uri = $this->getApplicationURI("/commit/edit/{$id}/");
$action = id(new PhabricatorActionView())
->setName(pht('Edit Commit'))
->setHref($edit_uri)
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$curtain->addAction($action);
$action = id(new PhabricatorActionView())
->setName(pht('Download Raw Diff'))
->setHref($request->getRequestURI()->alter('diff', true))
->setIcon('fa-download');
$curtain->addAction($action);
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$commit);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
return $curtain;
}
private function buildRawDiffResponse(DiffusionRequest $drequest) {
$diff_info = $this->callConduitWithDiffusionRequest(
'diffusion.rawdiffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
));
$file_phid = $diff_info['filePHID'];
$file = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery'));
}
return $file->getRedirectResponse();
}
private function renderAuditStatusView(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$viewer = $this->getViewer();
$view = new PHUIStatusListView();
foreach ($audit_requests as $request) {
$code = $request->getAuditStatus();
$item = new PHUIStatusItemView();
$item->setIcon(
PhabricatorAuditStatusConstants::getStatusIcon($code),
PhabricatorAuditStatusConstants::getStatusColor($code),
PhabricatorAuditStatusConstants::getStatusName($code));
$auditor_phid = $request->getAuditorPHID();
$target = $viewer->renderHandle($auditor_phid);
$item->setTarget($target);
if ($commit->hasAuditAuthority($viewer, $request)) {
$item->setHighlighted(true);
}
$view->addItem($item);
}
return $view;
}
private function linkBugtraq($corpus) {
$url = PhabricatorEnv::getEnvConfig('bugtraq.url');
if (!strlen($url)) {
return $corpus;
}
$regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex');
if (!$regexes) {
return $corpus;
}
$parser = id(new PhutilBugtraqParser())
->setBugtraqPattern("[[ {$url} | %BUGID% ]]")
->setBugtraqCaptureExpression(array_shift($regexes));
$select = array_shift($regexes);
if ($select) {
$parser->setBugtraqSelectExpression($select);
}
return $parser->processCorpus($corpus);
}
private function buildTableOfContents(
array $changesets,
$header,
$info_view) {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
$toc_view = id(new PHUIDiffTableOfContentsListView())
->setUser($viewer)
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
if ($info_view) {
$toc_view->setInfoView($info_view);
}
// TODO: This is hacky, we just want access to the linkX() methods on
// DiffusionView.
$diffusion_view = id(new DiffusionEmptyResultView())
->setDiffusionRequest($drequest);
$have_owners = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorOwnersApplication',
$viewer);
if (!$changesets) {
$have_owners = false;
}
if ($have_owners) {
if ($viewer->getPHID()) {
$packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->execute();
$toc_view->setAuthorityPackages($packages);
}
$repository = $drequest->getRepository();
$repository_phid = $repository->getPHID();
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withControl($repository_phid, mpull($changesets, 'getFilename'));
$control_query->execute();
}
foreach ($changesets as $changeset_id => $changeset) {
$path = $changeset->getFilename();
$anchor = $changeset->getAnchorName();
$history_link = $diffusion_view->linkHistory($path);
$browse_link = $diffusion_view->linkBrowse(
$path,
array(
'type' => $changeset->getFileType(),
));
$item = id(new PHUIDiffTableOfContentsItemView())
->setChangeset($changeset)
->setAnchor($anchor)
->setContext(
array(
$history_link,
' ',
$browse_link,
));
if ($have_owners) {
$packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$changeset->getFilename());
$item->setPackages($packages);
}
$toc_view->addItem($item);
}
return $toc_view;
}
private function loadCommitState() {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $drequest->getCommit();
// TODO: We could use futures here and resolve these calls in parallel.
$exceptions = array();
try {
$parent_refs = $this->callConduitWithDiffusionRequest(
'diffusion.commitparentsquery',
array(
'commit' => $commit,
));
if ($parent_refs) {
$parents = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers($parent_refs)
->execute();
} else {
$parents = array();
}
$this->commitParents = $parents;
} catch (Exception $ex) {
$this->commitParents = false;
$exceptions[] = $ex;
}
$merge_limit = $this->getMergeDisplayLimit();
try {
if ($repository->isSVN()) {
$this->commitMerges = array();
} else {
$merges = $this->callConduitWithDiffusionRequest(
'diffusion.mergedcommitsquery',
array(
'commit' => $commit,
'limit' => $merge_limit + 1,
));
$this->commitMerges = DiffusionPathChange::newFromConduit($merges);
}
} catch (Exception $ex) {
$this->commitMerges = false;
$exceptions[] = $ex;
}
try {
if ($repository->isGit()) {
$refs = $this->callConduitWithDiffusionRequest(
'diffusion.refsquery',
array(
'commit' => $commit,
));
} else {
$refs = array();
}
$this->commitRefs = $refs;
} catch (Exception $ex) {
$this->commitRefs = false;
$exceptions[] = $ex;
}
if ($exceptions) {
$exists = $this->callConduitWithDiffusionRequest(
'diffusion.existsquery',
array(
'commit' => $commit,
));
if ($exists) {
$this->commitExists = true;
foreach ($exceptions as $exception) {
$this->commitErrors[] = $exception->getMessage();
}
} else {
$this->commitExists = false;
$this->commitErrors[] = pht(
'This commit no longer exists in the repository. It may have '.
'been part of a branch which was deleted.');
}
} else {
$this->commitExists = true;
$this->commitErrors = array();
}
}
private function getMergeDisplayLimit() {
return 50;
}
private function getCommitExists() {
if ($this->commitExists === null) {
$this->loadCommitState();
}
return $this->commitExists;
}
private function getCommitParents() {
if ($this->commitParents === null) {
$this->loadCommitState();
}
return $this->commitParents;
}
private function getCommitRefs() {
if ($this->commitRefs === null) {
$this->loadCommitState();
}
return $this->commitRefs;
}
private function getCommitMerges() {
if ($this->commitMerges === null) {
$this->loadCommitState();
}
return $this->commitMerges;
}
private function getCommitErrors() {
if ($this->commitErrors === null) {
$this->loadCommitState();
}
return $this->commitErrors;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionIdentityEditController.php b/src/applications/diffusion/controller/DiffusionIdentityEditController.php
new file mode 100644
index 000000000..624a67285
--- /dev/null
+++ b/src/applications/diffusion/controller/DiffusionIdentityEditController.php
@@ -0,0 +1,12 @@
+<?php
+
+final class DiffusionIdentityEditController
+ extends DiffusionController {
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new PhabricatorRepositoryIdentityEditEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+}
diff --git a/src/applications/diffusion/controller/DiffusionIdentityListController.php b/src/applications/diffusion/controller/DiffusionIdentityListController.php
new file mode 100644
index 000000000..a6db3039c
--- /dev/null
+++ b/src/applications/diffusion/controller/DiffusionIdentityListController.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DiffusionIdentityListController
+ extends DiffusionController {
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new DiffusionRepositoryIdentitySearchEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ id(new PhabricatorRepositoryIdentityEditEngine())
+ ->setViewer($this->getViewer())
+ ->addActionToCrumbs($crumbs);
+
+ return $crumbs;
+ }
+
+}
diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php
new file mode 100644
index 000000000..20efe2749
--- /dev/null
+++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php
@@ -0,0 +1,135 @@
+<?php
+
+final class DiffusionIdentityViewController
+ extends DiffusionController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $id = $request->getURIData('id');
+ $identity = id(new PhabricatorRepositoryIdentityQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$identity) {
+ return new Aphront404Response();
+ }
+
+ $title = pht('Identity %d', $identity->getID());
+
+ $curtain = $this->buildCurtain($identity);
+
+ $header = id(new PHUIHeaderView())
+ ->setUser($viewer)
+ ->setHeader($identity->getIdentityShortName())
+ ->setHeaderIcon('fa-globe')
+ ->setPolicyObject($identity);
+
+ $crumbs = $this->buildApplicationCrumbs();
+ $crumbs->addTextCrumb($identity->getID());
+ $crumbs->setBorder(true);
+
+ $timeline = $this->buildTransactionTimeline(
+ $identity,
+ new PhabricatorRepositoryIdentityTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $properties = $this->buildPropertyList($identity);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(array(
+ $properties,
+ $timeline,
+ ));
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild(
+ array(
+ $view,
+ ));
+ }
+
+ private function buildCurtain(PhabricatorRepositoryIdentity $identity) {
+ $viewer = $this->getViewer();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $identity,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $id = $identity->getID();
+ $edit_uri = $this->getApplicationURI("identity/edit/{$id}/");
+
+ $curtain = $this->newCurtainView($identity);
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon('fa-pencil')
+ ->setName(pht('Edit Identity'))
+ ->setHref($edit_uri)
+ ->setWorkflow(!$can_edit)
+ ->setDisabled(!$can_edit));
+
+ return $curtain;
+ }
+
+ private function buildPropertyList(
+ PhabricatorRepositoryIdentity $identity) {
+
+ $viewer = $this->getViewer();
+
+ $properties = id(new PHUIPropertyListView())
+ ->setUser($viewer);
+
+ $effective_phid = $identity->getCurrentEffectiveUserPHID();
+ $automatic_phid = $identity->getAutomaticGuessedUserPHID();
+ $manual_phid = $identity->getManuallySetUserPHID();
+
+ if ($effective_phid) {
+ $tag = id(new PHUITagView())
+ ->setType(PHUITagView::TYPE_SHADE)
+ ->setColor('green')
+ ->setIcon('fa-check')
+ ->setName('Assigned');
+ } else {
+ $tag = id(new PHUITagView())
+ ->setType(PHUITagView::TYPE_SHADE)
+ ->setColor('indigo')
+ ->setIcon('fa-bomb')
+ ->setName('Unassigned');
+ }
+ $properties->addProperty(
+ pht('Effective User'),
+ $this->buildPropertyValue($effective_phid));
+ $properties->addProperty(
+ pht('Automatically Detected User'),
+ $this->buildPropertyValue($automatic_phid));
+ $properties->addProperty(
+ pht('Manually Set User'),
+ $this->buildPropertyValue($manual_phid));
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(array(pht('Identity Assignments'), $tag));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->addPropertyList($properties);
+ }
+
+ private function buildPropertyValue($value) {
+ $viewer = $this->getViewer();
+
+ if ($value == DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN) {
+ return phutil_tag('em', array(), pht('Explicitly Unassigned'));
+ } else if (!$value) {
+ return null;
+ } else {
+ return $viewer->renderHandle($value);
+ }
+ }
+}
diff --git a/src/applications/diffusion/controller/DiffusionInlineCommentController.php b/src/applications/diffusion/controller/DiffusionInlineCommentController.php
index 8af9a1cd6..ce69511dd 100644
--- a/src/applications/diffusion/controller/DiffusionInlineCommentController.php
+++ b/src/applications/diffusion/controller/DiffusionInlineCommentController.php
@@ -1,129 +1,139 @@
<?php
final class DiffusionInlineCommentController
extends PhabricatorInlineCommentController {
private function getCommitPHID() {
return $this->getRequest()->getURIData('phid');
}
private function loadCommit() {
$viewer = $this->getViewer();
$commit_phid = $this->getCommitPHID();
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($commit_phid))
->executeOne();
if (!$commit) {
throw new Exception(pht('Invalid commit PHID "%s"!', $commit_phid));
}
return $commit;
}
protected function createComment() {
$commit = $this->loadCommit();
// TODO: Write a real PathQuery object?
$path_id = $this->getChangesetID();
$path = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT path FROM %T WHERE id = %d',
PhabricatorRepository::TABLE_PATH,
$path_id);
if (!$path) {
throw new Exception(pht('Invalid path ID!'));
}
return id(new PhabricatorAuditInlineComment())
->setCommitPHID($commit->getPHID())
->setPathID($path_id);
}
protected function loadComment($id) {
return PhabricatorAuditInlineComment::loadID($id);
}
protected function loadCommentByPHID($phid) {
return PhabricatorAuditInlineComment::loadPHID($phid);
}
protected function loadCommentForEdit($id) {
- $request = $this->getRequest();
- $user = $request->getUser();
+ $viewer = $this->getViewer();
$inline = $this->loadComment($id);
- if (!$this->canEditInlineComment($user, $inline)) {
+ if (!$this->canEditInlineComment($viewer, $inline)) {
throw new Exception(pht('That comment is not editable!'));
}
return $inline;
}
protected function loadCommentForDone($id) {
- $request = $this->getRequest();
- $viewer = $request->getUser();
+ $viewer = $this->getViewer();
$inline = $this->loadComment($id);
if (!$inline) {
throw new Exception(pht('Failed to load comment "%d".', $id));
}
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($inline->getCommitPHID()))
->executeOne();
if (!$commit) {
throw new Exception(pht('Failed to load commit.'));
}
- if ((!$commit->getAuthorPHID()) ||
- ($commit->getAuthorPHID() != $viewer->getPHID())) {
- throw new Exception(pht('You can not mark this comment as complete.'));
+ $owner_phid = $commit->getAuthorPHID();
+ $viewer_phid = $viewer->getPHID();
+ $viewer_is_owner = ($owner_phid && ($owner_phid == $viewer_phid));
+ $viewer_is_author = ($viewer_phid == $inline->getAuthorPHID());
+ $is_draft = $inline->isDraft();
+
+ if ($viewer_is_owner) {
+ // You can mark inlines on your own commits as "Done".
+ } else if ($viewer_is_author && $is_draft) {
+ // You can mark your own unsubmitted inlines as "Done".
+ } else {
+ throw new Exception(
+ pht(
+ 'You can not mark this comment as complete: you did not author '.
+ 'the commit and the comment is not a draft you wrote.'));
}
return $inline;
}
private function canEditInlineComment(
- PhabricatorUser $user,
+ PhabricatorUser $viewer,
PhabricatorAuditInlineComment $inline) {
// Only the author may edit a comment.
- if ($inline->getAuthorPHID() != $user->getPHID()) {
+ if ($inline->getAuthorPHID() != $viewer->getPHID()) {
return false;
}
// Saved comments may not be edited.
if ($inline->getAuditCommentID()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getCommitPHID() != $this->getCommitPHID()) {
return false;
}
return true;
}
protected function deleteComment(PhabricatorInlineCommentInterface $inline) {
$inline->setIsDeleted(1)->save();
}
protected function undeleteComment(
PhabricatorInlineCommentInterface $inline) {
$inline->setIsDeleted(0)->save();
}
protected function saveComment(PhabricatorInlineCommentInterface $inline) {
return $inline->save();
}
protected function loadObjectOwnerPHID(
PhabricatorInlineCommentInterface $inline) {
return $this->loadCommit()->getAuthorPHID();
}
}
diff --git a/src/applications/diffusion/controller/DiffusionLastModifiedController.php b/src/applications/diffusion/controller/DiffusionLastModifiedController.php
index 945f8a58b..1a31d3a2b 100644
--- a/src/applications/diffusion/controller/DiffusionLastModifiedController.php
+++ b/src/applications/diffusion/controller/DiffusionLastModifiedController.php
@@ -1,173 +1,156 @@
<?php
final class DiffusionLastModifiedController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$paths = $request->getStr('paths');
try {
$paths = phutil_json_decode($paths);
} catch (PhutilJSONParserException $ex) {
return new Aphront400Response();
}
$modified_map = $this->callConduitWithDiffusionRequest(
'diffusion.lastmodifiedquery',
array(
'paths' => array_fill_keys($paths, $drequest->getCommit()),
));
if ($modified_map) {
$commit_map = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($drequest->getRepository())
->withIdentifiers(array_values($modified_map))
->needCommitData(true)
+ ->needIdentities(true)
->execute();
$commit_map = mpull($commit_map, null, 'getCommitIdentifier');
} else {
$commit_map = array();
}
$commits = array();
foreach ($paths as $path) {
$modified_at = idx($modified_map, $path);
if ($modified_at) {
$commit = idx($commit_map, $modified_at);
if ($commit) {
$commits[$path] = $commit;
}
}
}
$phids = array();
foreach ($commits as $commit) {
- $data = $commit->getCommitData();
- $phids[] = $data->getCommitDetail('authorPHID');
- $phids[] = $data->getCommitDetail('committerPHID');
+ $phids[] = $commit->getCommitterDisplayPHID();
+ $phids[] = $commit->getAuthorDisplayPHID();
}
$phids = array_filter($phids);
$handles = $this->loadViewerHandles($phids);
$branch = $drequest->loadBranch();
if ($branch && $commits) {
$lint_query = id(new DiffusionLintCountQuery())
->withBranchIDs(array($branch->getID()))
->withPaths(array_keys($commits));
if ($drequest->getLint()) {
$lint_query->withCodes(array($drequest->getLint()));
}
$lint = $lint_query->execute();
} else {
$lint = array();
}
$output = array();
foreach ($commits as $path => $commit) {
$prequest = clone $drequest;
$prequest->setPath($path);
$output[$path] = $this->renderColumns(
$prequest,
$handles,
$commit,
idx($lint, $path));
}
return id(new AphrontAjaxResponse())->setContent($output);
}
private function renderColumns(
DiffusionRequest $drequest,
array $handles,
PhabricatorRepositoryCommit $commit = null,
$lint = null) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$viewer = $this->getRequest()->getUser();
if ($commit) {
$epoch = $commit->getEpoch();
$modified = DiffusionView::linkCommit(
$drequest->getRepository(),
$commit->getCommitIdentifier());
$date = $viewer->formatShortDateTime($epoch);
} else {
$modified = '';
$date = '';
}
- $data = $commit->getCommitData();
- if ($data) {
- $author_phid = $data->getCommitDetail('authorPHID');
- if ($author_phid && isset($handles[$author_phid])) {
- $author = $handles[$author_phid]->renderLink();
- } else {
- $author = DiffusionView::renderName($data->getAuthorName());
- }
-
- $committer = $data->getCommitDetail('committer');
- if ($committer) {
- $committer_phid = $data->getCommitDetail('committerPHID');
- if ($committer_phid && isset($handles[$committer_phid])) {
- $committer = $handles[$committer_phid]->renderLink();
- } else {
- $committer = DiffusionView::renderName($committer);
- }
- if ($author != $committer) {
- $author = hsprintf('%s/%s', $author, $committer);
- }
- }
+ $author = $commit->renderAuthor($viewer, $handles);
+ $committer = $commit->renderCommitter($viewer, $handles);
- $details = DiffusionView::linkDetail(
- $drequest->getRepository(),
- $commit->getCommitIdentifier(),
- $data->getSummary());
- $details = AphrontTableView::renderSingleDisplayLine($details);
- } else {
- $author = '';
- $details = '';
+ if ($author != $committer) {
+ $author = hsprintf('%s/%s', $author, $committer);
}
+ $data = $commit->getCommitData();
+ $details = DiffusionView::linkDetail(
+ $drequest->getRepository(),
+ $commit->getCommitIdentifier(),
+ $data->getSummary());
+ $details = AphrontTableView::renderSingleDisplayLine($details);
+
+
$return = array(
'commit' => $modified,
'date' => $date,
'author' => $author,
'details' => $details,
);
if ($lint !== null) {
$return['lint'] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(array(
'action' => 'lint',
'lint' => null,
)),
),
number_format($lint));
}
// The client treats these results as markup, so make sure they have been
// escaped correctly.
foreach ($return as $key => $value) {
$return[$key] = hsprintf('%s', $value);
}
return $return;
}
}
diff --git a/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php b/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php
index 17abba5c4..800d19572 100644
--- a/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php
+++ b/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php
@@ -1,102 +1,111 @@
<?php
final class DiffusionDocumentRenderingEngine
extends PhabricatorDocumentRenderingEngine {
private $diffusionRequest;
public function setDiffusionRequest(DiffusionRequest $drequest) {
$this->diffusionRequest = $drequest;
return $this;
}
public function getDiffusionRequest() {
return $this->diffusionRequest;
}
protected function newRefViewURI(
PhabricatorDocumentRef $ref,
PhabricatorDocumentEngine $engine) {
$file = $ref->getFile();
$engine_key = $engine->getDocumentEngineKey();
$drequest = $this->getDiffusionRequest();
return (string)$drequest->generateURI(
array(
'action' => 'browse',
'stable' => true,
'params' => array(
'as' => $engine_key,
),
));
}
protected function newRefRenderURI(
PhabricatorDocumentRef $ref,
PhabricatorDocumentEngine $engine) {
$engine_key = $engine->getDocumentEngineKey();
$file = $ref->getFile();
$file_phid = $file->getPHID();
$drequest = $this->getDiffusionRequest();
return (string)$drequest->generateURI(
array(
'action' => 'document',
'stable' => true,
'params' => array(
'as' => $engine_key,
'filePHID' => $file_phid,
),
));
}
protected function getSelectedDocumentEngineKey() {
return $this->getRequest()->getStr('as');
}
protected function getSelectedLineRange() {
$range = $this->getDiffusionRequest()->getLine();
return AphrontRequest::parseURILineRange($range, 1000);
}
protected function addApplicationCrumbs(
PHUICrumbsView $crumbs,
PhabricatorDocumentRef $ref = null) {
return;
}
- protected function willRenderRef(PhabricatorDocumentRef $ref) {
+ protected function willStageRef(PhabricatorDocumentRef $ref) {
$drequest = $this->getDiffusionRequest();
$blame_uri = (string)$drequest->generateURI(
array(
'action' => 'blame',
'stable' => true,
));
- $ref
- ->setSymbolMetadata($this->getSymbolMetadata())
- ->setBlameURI($blame_uri);
+ $ref->setBlameURI($blame_uri);
+ }
+
+ protected function willRenderRef(PhabricatorDocumentRef $ref) {
+ $drequest = $this->getDiffusionRequest();
+
+ $ref->setSymbolMetadata($this->getSymbolMetadata());
+
+ $coverage = $drequest->loadCoverage();
+ if (strlen($coverage)) {
+ $ref->addCoverage($coverage);
+ }
}
private function getSymbolMetadata() {
$drequest = $this->getDiffusionRequest();
$repo = $drequest->getRepository();
$symbol_repos = nonempty($repo->getSymbolSources(), array());
$symbol_repos[] = $repo->getPHID();
$lang = last(explode('.', $drequest->getPath()));
return array(
'repositories' => $symbol_repos,
'lang' => $lang,
'path' => $drequest->getPath(),
);
}
}
diff --git a/src/applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php b/src/applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php
new file mode 100644
index 000000000..3a423809e
--- /dev/null
+++ b/src/applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DiffusionIdentityAssigneeEditField
+ extends PhabricatorTokenizerEditField {
+
+ protected function newDatasource() {
+ return new DiffusionIdentityAssigneeDatasource();
+ }
+
+ protected function newHTTPParameterType() {
+ return new AphrontUserListHTTPParameterType();
+ }
+
+ protected function newConduitParameterType() {
+ if ($this->getIsSingleValue()) {
+ return new ConduitUserParameterType();
+ } else {
+ return new ConduitUserListParameterType();
+ }
+ }
+
+}
diff --git a/src/applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php b/src/applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php
new file mode 100644
index 000000000..8e964e3e7
--- /dev/null
+++ b/src/applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php
@@ -0,0 +1,26 @@
+<?php
+
+final class DiffusionRepositoryIdentityEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorObjectsDescription() {
+ return pht('Repository Identity');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this identity.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+ protected function supportsSearch() {
+ return true;
+ }
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorDiffusionApplication';
+ }
+
+}
diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index baffc82b1..ba4af1392 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,1350 +1,1382 @@
<?php
/**
* @task config Configuring the Hook Engine
* @task hook Hook Execution
* @task git Git Hooks
* @task hg Mercurial Hooks
* @task svn Subversion Hooks
* @task internal Internals
*/
final class DiffusionCommitHookEngine extends Phobject {
const ENV_REPOSITORY = 'PHABRICATOR_REPOSITORY';
const ENV_USER = 'PHABRICATOR_USER';
const ENV_REQUEST = 'PHABRICATOR_REQUEST';
const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS';
const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL';
const EMPTY_HASH = '0000000000000000000000000000000000000000';
private $viewer;
private $repository;
private $stdin;
private $originalArgv;
private $subversionTransaction;
private $subversionRepository;
private $remoteAddress;
private $remoteProtocol;
private $requestIdentifier;
private $transactionKey;
private $mercurialHook;
private $mercurialCommits = array();
private $gitCommits = array();
private $heraldViewerProjects;
private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN;
private $rejectDetails;
private $emailPHIDs = array();
private $changesets = array();
+ private $changesetsSize = 0;
/* -( Config )------------------------------------------------------------- */
public function setRemoteProtocol($remote_protocol) {
$this->remoteProtocol = $remote_protocol;
return $this;
}
public function getRemoteProtocol() {
return $this->remoteProtocol;
}
public function setRemoteAddress($remote_address) {
$this->remoteAddress = $remote_address;
return $this;
}
public function getRemoteAddress() {
return $this->remoteAddress;
}
public function setRequestIdentifier($request_identifier) {
$this->requestIdentifier = $request_identifier;
return $this;
}
public function getRequestIdentifier() {
return $this->requestIdentifier;
}
public function setSubversionTransactionInfo($transaction, $repository) {
$this->subversionTransaction = $transaction;
$this->subversionRepository = $repository;
return $this;
}
// c4science customization
public function getSubversionTransaction() {
return $this->subversionTransaction;
}
public function setStdin($stdin) {
$this->stdin = $stdin;
return $this;
}
public function getStdin() {
return $this->stdin;
}
public function setOriginalArgv(array $original_argv) {
$this->originalArgv = $original_argv;
return $this;
}
public function getOriginalArgv() {
return $this->originalArgv;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setMercurialHook($mercurial_hook) {
$this->mercurialHook = $mercurial_hook;
return $this;
}
public function getMercurialHook() {
return $this->mercurialHook;
}
/* -( Hook Execution )----------------------------------------------------- */
public function execute() {
$ref_updates = $this->findRefUpdates();
$all_updates = $ref_updates;
$caught = null;
try {
try {
$this->rejectDangerousChanges($ref_updates);
} catch (DiffusionCommitHookRejectException $ex) {
// If we're rejecting dangerous changes, flag everything that we've
// seen as rejected so it's clear that none of it was accepted.
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS;
throw $ex;
}
$content_updates = $this->findContentUpdates($ref_updates);
$all_updates = array_merge($ref_updates, $content_updates);
// If this is an "initial import" (a sizable push to a previously empty
// repository) we'll allow enormous changes and disable Herald rules.
// These rulesets can consume a large amount of time and memory and are
// generally not relevant when importing repository history.
$is_initial_import = $this->isInitialImport($all_updates);
if (!$is_initial_import) {
$this->applyHeraldRefRules($ref_updates);
}
try {
if (!$is_initial_import) {
$this->rejectEnormousChanges($content_updates);
}
} catch (DiffusionCommitHookRejectException $ex) {
// If we're rejecting enormous changes, flag everything.
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS;
throw $ex;
}
if (!$is_initial_import) {
$this->applyHeraldContentRules($content_updates);
}
// Run custom scripts in `hook.d/` directories.
$this->applyCustomHooks($all_updates);
// If we make it this far, we're accepting these changes. Mark all the
// logs as accepted.
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT;
} catch (Exception $ex) {
// We'll throw this again in a minute, but we want to save all the logs
// first.
$caught = $ex;
}
// Save all the logs no matter what the outcome was.
$event = $this->newPushEvent();
$event->setRejectCode($this->rejectCode);
$event->setRejectDetails($this->rejectDetails);
$event->openTransaction();
$event->save();
foreach ($all_updates as $update) {
$update->setPushEventPHID($event->getPHID());
$update->save();
}
$event->saveTransaction();
if ($caught) {
throw $caught;
}
// If this went through cleanly and was an import, set the importing flag
// on the repository. It will be cleared once we fully process everything.
if ($is_initial_import) {
$repository = $this->getRepository();
$repository->markImporting();
}
if ($this->emailPHIDs) {
// If Herald rules triggered email to users, queue a worker to send the
// mail. We do this out-of-process so that we block pushes as briefly
// as possible.
// (We do need to pull some commit info here because the commit objects
// may not exist yet when this worker runs, which could be immediately.)
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryPushMailWorker',
array(
'eventPHID' => $event->getPHID(),
'emailPHIDs' => array_values($this->emailPHIDs),
'info' => $this->loadCommitInfoForWorker($all_updates),
),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
}
return 0;
}
private function findRefUpdates() {
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return $this->findGitRefUpdates();
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return $this->findMercurialRefUpdates();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->findSubversionRefUpdates();
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
}
private function rejectDangerousChanges(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
$repository = $this->getRepository();
if ($repository->shouldAllowDangerousChanges()) {
return;
}
$flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
foreach ($ref_updates as $ref_update) {
if (!$ref_update->hasChangeFlags($flag_dangerous)) {
// This is not a dangerous change.
continue;
}
// We either have a branch deletion or a non fast-forward branch update.
// Format a message and reject the push.
$message = pht(
"DANGEROUS CHANGE: %s\n".
"Dangerous change protection is enabled for this repository.\n".
"Edit the repository configuration before making dangerous changes.",
$ref_update->getDangerousChangeDescription());
throw new DiffusionCommitHookRejectException($message);
}
}
private function findContentUpdates(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return $this->findGitContentUpdates($ref_updates);
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return $this->findMercurialContentUpdates($ref_updates);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->findSubversionContentUpdates($ref_updates);
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
}
/* -( Herald )------------------------------------------------------------- */
private function applyHeraldRefRules(array $ref_updates) {
$this->applyHeraldRules(
$ref_updates,
new HeraldPreCommitRefAdapter());
}
private function applyHeraldContentRules(array $content_updates) {
$this->applyHeraldRules(
$content_updates,
new HeraldPreCommitContentAdapter());
}
private function applyHeraldRules(
array $updates,
HeraldAdapter $adapter_template) {
if (!$updates) {
return;
}
$viewer = $this->getViewer();
$adapter_template
->setHookEngine($this)
->setActingAsPHID($viewer->getPHID());
$engine = new HeraldEngine();
$rules = null;
$blocking_effect = null;
$blocked_update = null;
$blocking_xscript = null;
foreach ($updates as $update) {
$adapter = id(clone $adapter_template)
->setPushLog($update);
if ($rules === null) {
$rules = $engine->loadRulesForAdapter($adapter);
}
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
$xscript = $engine->getTranscript();
// Store any PHIDs we want to send email to for later.
foreach ($adapter->getEmailPHIDs() as $email_phid) {
$this->emailPHIDs[$email_phid] = $email_phid;
}
$block_action = DiffusionBlockHeraldAction::ACTIONCONST;
if ($blocking_effect === null) {
foreach ($effects as $effect) {
if ($effect->getAction() == $block_action) {
$blocking_effect = $effect;
$blocked_update = $update;
$blocking_xscript = $xscript;
break;
}
}
}
}
if ($blocking_effect) {
$rule = $blocking_effect->getRule();
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD;
$this->rejectDetails = $rule->getPHID();
$message = $blocking_effect->getTarget();
if (!strlen($message)) {
$message = pht('(None.)');
}
$blocked_ref_name = coalesce(
$blocked_update->getRefName(),
$blocked_update->getRefNewShort());
$blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name;
throw new DiffusionCommitHookRejectException(
pht(
"This push was rejected by Herald push rule %s.\n".
" Change: %s\n".
" Rule: %s\n".
" Reason: %s\n".
"Transcript: %s",
$rule->getMonogram(),
$blocked_name,
$rule->getName(),
$message,
PhabricatorEnv::getProductionURI(
'/herald/transcript/'.$blocking_xscript->getID().'/')));
}
}
public function loadViewerProjectPHIDsForHerald() {
// This just caches the viewer's projects so we don't need to load them
// over and over again when applying Herald rules.
if ($this->heraldViewerProjects === null) {
$this->heraldViewerProjects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs(array($this->getViewer()->getPHID()))
->execute();
}
return mpull($this->heraldViewerProjects, 'getPHID');
}
/* -( Git )---------------------------------------------------------------- */
private function findGitRefUpdates() {
$ref_updates = array();
// First, parse stdin, which lists all the ref changes. The input looks
// like this:
//
// <old hash> <new hash> <ref>
$stdin = $this->getStdin();
$lines = phutil_split_lines($stdin, $retain_endings = false);
foreach ($lines as $line) {
$parts = explode(' ', $line, 3);
if (count($parts) != 3) {
throw new Exception(pht('Expected "old new ref", got "%s".', $line));
}
$ref_old = $parts[0];
$ref_new = $parts[1];
$ref_raw = $parts[2];
if (preg_match('(^refs/heads/)', $ref_raw)) {
$ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
$ref_raw = substr($ref_raw, strlen('refs/heads/'));
} else if (preg_match('(^refs/tags/)', $ref_raw)) {
$ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG;
$ref_raw = substr($ref_raw, strlen('refs/tags/'));
} else {
throw new Exception(
pht(
"Unable to identify the reftype of '%s'. Rejecting push.",
$ref_raw));
}
$ref_update = $this->newPushLog()
->setRefType($ref_type)
->setRefName($ref_raw)
->setRefOld($ref_old)
->setRefNew($ref_new);
$ref_updates[] = $ref_update;
}
$this->findGitMergeBases($ref_updates);
$this->findGitChangeFlags($ref_updates);
return $ref_updates;
}
private function findGitMergeBases(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
$futures = array();
foreach ($ref_updates as $key => $ref_update) {
// If the old hash is "00000...", the ref is being created (either a new
// branch, or a new tag). If the new hash is "00000...", the ref is being
// deleted. If both are nonempty, the ref is being updated. For updates,
// we'll figure out the `merge-base` of the old and new objects here. This
// lets us reject non-FF changes cheaply; later, we'll figure out exactly
// which commits are new.
$ref_old = $ref_update->getRefOld();
$ref_new = $ref_update->getRefNew();
if (($ref_old === self::EMPTY_HASH) ||
($ref_new === self::EMPTY_HASH)) {
continue;
}
$futures[$key] = $this->getRepository()->getLocalCommandFuture(
'merge-base %s %s',
$ref_old,
$ref_new);
}
$futures = id(new FutureIterator($futures))
->limit(8);
foreach ($futures as $key => $future) {
// If 'old' and 'new' have no common ancestors (for example, a force push
// which completely rewrites a ref), `git merge-base` will exit with
// an error and no output. It would be nice to find a positive test
// for this instead, but I couldn't immediately come up with one. See
// T4224. Assume this means there are no ancestors.
list($err, $stdout) = $future->resolve();
if ($err) {
$merge_base = null;
} else {
$merge_base = rtrim($stdout, "\n");
}
$ref_update = $ref_updates[$key];
$ref_update->setMergeBase($merge_base);
}
return $ref_updates;
}
private function findGitChangeFlags(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
foreach ($ref_updates as $key => $ref_update) {
$ref_old = $ref_update->getRefOld();
$ref_new = $ref_update->getRefNew();
$ref_type = $ref_update->getRefType();
$ref_flags = 0;
$dangerous = null;
if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) {
// This happens if you try to delete a tag or branch which does not
// exist by pushing directly to the ref. Git will warn about it but
// allow it. Just call it a delete, without flagging it as dangerous.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
} else if ($ref_old === self::EMPTY_HASH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
} else if ($ref_new === self::EMPTY_HASH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
$dangerous = pht(
"The change you're attempting to push deletes the branch '%s'.",
$ref_update->getRefName());
}
} else {
$merge_base = $ref_update->getMergeBase();
if ($merge_base == $ref_old) {
// This is a fast-forward update to an existing branch.
// These are safe.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
} else {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
// For now, we don't consider deleting or moving tags to be a
// "dangerous" update. It's way harder to get wrong and should be easy
// to recover from once we have better logging. Only add the dangerous
// flag if this ref is a branch.
if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
$dangerous = pht(
"The change you're attempting to push updates the branch '%s' ".
"from '%s' to '%s', but this is not a fast-forward. Pushes ".
"which rewrite published branch history are dangerous.",
$ref_update->getRefName(),
$ref_update->getRefOldShort(),
$ref_update->getRefNewShort());
}
}
}
$ref_update->setChangeFlags($ref_flags);
if ($dangerous !== null) {
$ref_update->attachDangerousChangeDescription($dangerous);
}
}
return $ref_updates;
}
private function findGitContentUpdates(array $ref_updates) {
$flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
$futures = array();
foreach ($ref_updates as $key => $ref_update) {
if ($ref_update->hasChangeFlags($flag_delete)) {
// Deleting a branch or tag can never create any new commits.
continue;
}
// NOTE: This piece of magic finds all new commits, by walking backward
// from the new value to the value of *any* existing ref in the
// repository. Particularly, this will cover the cases of a new branch, a
// completely moved tag, etc.
$futures[$key] = $this->getRepository()->getLocalCommandFuture(
'log --format=%s %s --not --all',
'%H',
$ref_update->getRefNew());
}
$content_updates = array();
$futures = id(new FutureIterator($futures))
->limit(8);
foreach ($futures as $key => $future) {
list($stdout) = $future->resolvex();
if (!strlen(trim($stdout))) {
// This change doesn't have any new commits. One common case of this
// is creating a new tag which points at an existing commit.
continue;
}
$commits = phutil_split_lines($stdout, $retain_newlines = false);
// If we're looking at a branch, mark all of the new commits as on that
// branch. It's only possible for these commits to be on updated branches,
// since any other branch heads are necessarily behind them.
$branch_name = null;
$ref_update = $ref_updates[$key];
$type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
if ($ref_update->getRefType() == $type_branch) {
$branch_name = $ref_update->getRefName();
}
foreach ($commits as $commit) {
if ($branch_name) {
$this->gitCommits[$commit][] = $branch_name;
}
$content_updates[$commit] = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
->setRefNew($commit)
->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
}
}
return $content_updates;
}
/* -( Custom )------------------------------------------------------------- */
private function applyCustomHooks(array $updates) {
$args = $this->getOriginalArgv();
$stdin = $this->getStdin();
$console = PhutilConsole::getConsole();
$env = array(
self::ENV_REPOSITORY => $this->getRepository()->getPHID(),
self::ENV_USER => $this->getViewer()->getUsername(),
self::ENV_REQUEST => $this->getRequestIdentifier(),
self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(),
self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(),
);
$repository = $this->getRepository();
$env += $repository->getPassthroughEnvironmentalVariables();
$directories = $repository->getHookDirectories();
foreach ($directories as $directory) {
$hooks = $this->getExecutablesInDirectory($directory);
sort($hooks);
foreach ($hooks as $hook) {
// NOTE: We're explicitly running the hooks in sequential order to
// make this more predictable.
$future = id(new ExecFuture('%s %Ls', $hook, $args))
->setEnv($env, $wipe_process_env = false)
->write($stdin);
list($err, $stdout, $stderr) = $future->resolve();
if (!$err) {
// This hook ran OK, but echo its output in case there was something
// informative.
$console->writeOut('%s', $stdout);
$console->writeErr('%s', $stderr);
continue;
}
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL;
$this->rejectDetails = basename($hook);
throw new DiffusionCommitHookRejectException(
pht(
"This push was rejected by custom hook script '%s':\n\n%s%s",
basename($hook),
$stdout,
$stderr));
}
}
}
private function getExecutablesInDirectory($directory) {
$executables = array();
if (!Filesystem::pathExists($directory)) {
return $executables;
}
foreach (Filesystem::listDirectory($directory) as $path) {
$full_path = $directory.DIRECTORY_SEPARATOR.$path;
if (!is_executable($full_path)) {
// Don't include non-executable files.
continue;
}
if (basename($full_path) == 'README') {
// Don't include README, even if it is marked as executable. It almost
// certainly got caught in the crossfire of a sweeping `chmod`, since
// users do this with some frequency.
continue;
}
$executables[] = $full_path;
}
return $executables;
}
/* -( Mercurial )---------------------------------------------------------- */
private function findMercurialRefUpdates() {
$hook = $this->getMercurialHook();
switch ($hook) {
case 'pretxnchangegroup':
return $this->findMercurialChangegroupRefUpdates();
case 'prepushkey':
return $this->findMercurialPushKeyRefUpdates();
default:
throw new Exception(pht('Unrecognized hook "%s"!', $hook));
}
}
private function findMercurialChangegroupRefUpdates() {
$hg_node = getenv('HG_NODE');
if (!$hg_node) {
throw new Exception(
pht(
'Expected %s in environment!',
'HG_NODE'));
}
// NOTE: We need to make sure this is passed to subprocesses, or they won't
// be able to see new commits. Mercurial uses this as a marker to determine
// whether the pending changes are visible or not.
$_ENV['HG_PENDING'] = getenv('HG_PENDING');
$repository = $this->getRepository();
$futures = array();
foreach (array('old', 'new') as $key) {
$futures[$key] = $repository->getLocalCommandFuture(
'heads --template %s',
'{node}\1{branch}\2');
}
// Wipe HG_PENDING out of the old environment so we see the pre-commit
// state of the repository.
$futures['old']->updateEnv('HG_PENDING', null);
$futures['commits'] = $repository->getLocalCommandFuture(
'log --rev %s --template %s',
hgsprintf('%s:%s', $hg_node, 'tip'),
'{node}\1{branch}\2');
// Resolve all of the futures now. We don't need the 'commits' future yet,
// but it simplifies the logic to just get it out of the way.
foreach (new FutureIterator($futures) as $future) {
$future->resolve();
}
list($commit_raw) = $futures['commits']->resolvex();
$commit_map = $this->parseMercurialCommits($commit_raw);
$this->mercurialCommits = $commit_map;
// NOTE: `hg heads` exits with an error code and no output if the repository
// has no heads. Most commonly this happens on a new repository. We know
// we can run `hg` successfully since the `hg log` above didn't error, so
// just ignore the error code.
list($err, $old_raw) = $futures['old']->resolve();
$old_refs = $this->parseMercurialHeads($old_raw);
list($err, $new_raw) = $futures['new']->resolve();
$new_refs = $this->parseMercurialHeads($new_raw);
$all_refs = array_keys($old_refs + $new_refs);
$ref_updates = array();
foreach ($all_refs as $ref) {
$old_heads = idx($old_refs, $ref, array());
$new_heads = idx($new_refs, $ref, array());
sort($old_heads);
sort($new_heads);
if (!$old_heads && !$new_heads) {
// This should never be possible, as it makes no sense. Explode.
throw new Exception(
pht(
'Mercurial repository has no new or old heads for branch "%s" '.
'after push. This makes no sense; rejecting change.',
$ref));
}
if ($old_heads === $new_heads) {
// No changes to this branch, so skip it.
continue;
}
$stray_heads = array();
$head_map = array();
if ($old_heads && !$new_heads) {
// This is a branch deletion with "--close-branch".
foreach ($old_heads as $old_head) {
$head_map[$old_head] = array(self::EMPTY_HASH);
}
} else if (count($old_heads) > 1) {
// HORRIBLE: In Mercurial, branches can have multiple heads. If the
// old branch had multiple heads, we need to figure out which new
// heads descend from which old heads, so we can tell whether you're
// actively creating new heads (dangerous) or just working in a
// repository that's already full of garbage (strongly discouraged but
// not as inherently dangerous). These cases should be very uncommon.
// NOTE: We're only looking for heads on the same branch. The old
// tip of the branch may be the branchpoint for other branches, but that
// is OK.
$dfutures = array();
foreach ($old_heads as $old_head) {
$dfutures[$old_head] = $repository->getLocalCommandFuture(
'log --branch %s --rev %s --template %s',
$ref,
hgsprintf('(descendants(%s) and head())', $old_head),
'{node}\1');
}
foreach (new FutureIterator($dfutures) as $future_head => $dfuture) {
list($stdout) = $dfuture->resolvex();
$descendant_heads = array_filter(explode("\1", $stdout));
if ($descendant_heads) {
// This old head has at least one descendant in the push.
$head_map[$future_head] = $descendant_heads;
} else {
// This old head has no descendants, so it is being deleted.
$head_map[$future_head] = array(self::EMPTY_HASH);
}
}
// Now, find all the new stray heads this push creates, if any. These
// are new heads which do not descend from the old heads.
$seen = array_fuse(array_mergev($head_map));
foreach ($new_heads as $new_head) {
if ($new_head === self::EMPTY_HASH) {
// If a branch head is being deleted, don't insert it as an add.
continue;
}
if (empty($seen[$new_head])) {
$head_map[self::EMPTY_HASH][] = $new_head;
}
}
} else if ($old_heads) {
$head_map[head($old_heads)] = $new_heads;
} else {
$head_map[self::EMPTY_HASH] = $new_heads;
}
foreach ($head_map as $old_head => $child_heads) {
foreach ($child_heads as $new_head) {
if ($new_head === $old_head) {
continue;
}
$ref_flags = 0;
$dangerous = null;
if ($old_head == self::EMPTY_HASH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
} else {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
}
$deletes_existing_head = ($new_head == self::EMPTY_HASH);
$splits_existing_head = (count($child_heads) > 1);
$creates_duplicate_head = ($old_head == self::EMPTY_HASH) &&
(count($head_map) > 1);
if ($splits_existing_head || $creates_duplicate_head) {
$readable_child_heads = array();
foreach ($child_heads as $child_head) {
$readable_child_heads[] = substr($child_head, 0, 12);
}
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
if ($splits_existing_head) {
// We're splitting an existing head into two or more heads.
// This is dangerous, and a super bad idea. Note that we're only
// raising this if you're actively splitting a branch head. If a
// head split in the past, we don't consider appends to it
// to be dangerous.
$dangerous = pht(
"The change you're attempting to push splits the head of ".
"branch '%s' into multiple heads: %s. This is inadvisable ".
"and dangerous.",
$ref,
implode(', ', $readable_child_heads));
} else {
// We're adding a second (or more) head to a branch. The new
// head is not a descendant of any old head.
$dangerous = pht(
"The change you're attempting to push creates new, divergent ".
"heads for the branch '%s': %s. This is inadvisable and ".
"dangerous.",
$ref,
implode(', ', $readable_child_heads));
}
}
if ($deletes_existing_head) {
// TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE
// if we are also creating at least one other head to replace
// this one.
// NOTE: In Git, this is a dangerous change, but it is not dangerous
// in Mercurial. Mercurial branches are version controlled, and
// Mercurial does not prompt you for any special flags when pushing
// a `--close-branch` commit by default.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
}
$ref_update = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH)
->setRefName($ref)
->setRefOld($old_head)
->setRefNew($new_head)
->setChangeFlags($ref_flags);
if ($dangerous !== null) {
$ref_update->attachDangerousChangeDescription($dangerous);
}
$ref_updates[] = $ref_update;
}
}
}
return $ref_updates;
}
private function findMercurialPushKeyRefUpdates() {
$key_namespace = getenv('HG_NAMESPACE');
if ($key_namespace === 'phases') {
// Mercurial changes commit phases as part of normal push operations. We
// just ignore these, as they don't seem to represent anything
// interesting.
return array();
}
$key_name = getenv('HG_KEY');
$key_old = getenv('HG_OLD');
if (!strlen($key_old)) {
$key_old = null;
}
$key_new = getenv('HG_NEW');
if (!strlen($key_new)) {
$key_new = null;
}
if ($key_namespace !== 'bookmarks') {
throw new Exception(
pht(
"Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ".
"Rejecting push.",
$key_namespace,
$key_name,
coalesce($key_old, pht('null')),
coalesce($key_new, pht('null'))));
}
if ($key_old === $key_new) {
// We get a callback when the bookmark doesn't change. Just ignore this,
// as it's a no-op.
return array();
}
$ref_flags = 0;
$merge_base = null;
if ($key_old === null) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
} else if ($key_new === null) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
} else {
list($merge_base_raw) = $this->getRepository()->execxLocalCommand(
'log --template %s --rev %s',
'{node}',
hgsprintf('ancestor(%s, %s)', $key_old, $key_new));
if (strlen(trim($merge_base_raw))) {
$merge_base = trim($merge_base_raw);
}
if ($merge_base && ($merge_base === $key_old)) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
} else {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
}
}
$ref_update = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK)
->setRefName($key_name)
->setRefOld(coalesce($key_old, self::EMPTY_HASH))
->setRefNew(coalesce($key_new, self::EMPTY_HASH))
->setChangeFlags($ref_flags);
return array($ref_update);
}
private function findMercurialContentUpdates(array $ref_updates) {
$content_updates = array();
foreach ($this->mercurialCommits as $commit => $branches) {
$content_updates[$commit] = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
->setRefNew($commit)
->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
}
return $content_updates;
}
private function parseMercurialCommits($raw) {
$commits_lines = explode("\2", $raw);
$commits_lines = array_filter($commits_lines);
$commit_map = array();
foreach ($commits_lines as $commit_line) {
list($node, $branch) = explode("\1", $commit_line);
$commit_map[$node] = array($branch);
}
return $commit_map;
}
private function parseMercurialHeads($raw) {
$heads_map = $this->parseMercurialCommits($raw);
$heads = array();
foreach ($heads_map as $commit => $branches) {
foreach ($branches as $branch) {
$heads[$branch][] = $commit;
}
}
return $heads;
}
/* -( Subversion )--------------------------------------------------------- */
private function findSubversionRefUpdates() {
// Subversion doesn't have any kind of mutable ref metadata.
return array();
}
private function findSubversionContentUpdates(array $ref_updates) {
list($youngest) = execx(
'svnlook youngest %s',
$this->subversionRepository);
$ref_new = (int)$youngest + 1;
$ref_flags = 0;
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
$ref_content = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
->setRefNew($ref_new)
->setChangeFlags($ref_flags);
return array($ref_content);
}
/* -( Internals )---------------------------------------------------------- */
private function newPushLog() {
// NOTE: We generate PHIDs up front so the Herald transcripts can pick them
// up.
$phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
$device = AlmanacKeys::getLiveDevice();
if ($device) {
$device_phid = $device->getPHID();
} else {
$device_phid = null;
}
return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
->setPHID($phid)
->setDevicePHID($device_phid)
->setRepositoryPHID($this->getRepository()->getPHID())
->attachRepository($this->getRepository())
->setEpoch(PhabricatorTime::getNow());
}
private function newPushEvent() {
$viewer = $this->getViewer();
$event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer)
->setRepositoryPHID($this->getRepository()->getPHID())
->setRemoteAddress($this->getRemoteAddress())
->setRemoteProtocol($this->getRemoteProtocol())
->setEpoch(PhabricatorTime::getNow());
$identifier = $this->getRequestIdentifier();
if (strlen($identifier)) {
$event->setRequestIdentifier($identifier);
}
return $event;
}
private function rejectEnormousChanges(array $content_updates) {
$repository = $this->getRepository();
if ($repository->shouldAllowEnormousChanges()) {
return;
}
+ // See T13142. Don't cache more than 64MB of changesets. For normal small
+ // pushes, caching everything here can let us hit the cache from Herald if
+ // we need to run content rules, which speeds things up a bit. For large
+ // pushes, we may not be able to hold everything in memory.
+ $cache_limit = 1024 * 1024 * 64;
+
foreach ($content_updates as $update) {
$identifier = $update->getRefNew();
try {
- $changesets = $this->loadChangesetsForCommit($identifier);
- $this->changesets[$identifier] = $changesets;
+ $info = $this->loadChangesetsForCommit($identifier);
+ list($changesets, $size) = $info;
+
+ if ($this->changesetsSize + $size <= $cache_limit) {
+ $this->changesets[$identifier] = $changesets;
+ $this->changesetsSize += $size;
+ }
} catch (Exception $ex) {
$this->changesets[$identifier] = $ex;
$message = pht(
'ENORMOUS CHANGE'.
"\n".
'Enormous change protection is enabled for this repository, but '.
'you are pushing an enormous change ("%s"). Edit the repository '.
'configuration before making enormous changes.'.
"\n\n".
"Content Exception: %s",
$identifier,
$ex->getMessage());
throw new DiffusionCommitHookRejectException($message);
}
}
}
private function loadChangesetsForCommit($identifier) {
$byte_limit = HeraldCommitAdapter::getEnormousByteLimit();
$time_limit = HeraldCommitAdapter::getEnormousTimeLimit();
$vcs = $this->getRepository()->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// For git and hg, we can use normal commands.
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $this->getRepository(),
'user' => $this->getViewer(),
'commit' => $identifier,
));
$raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
->setTimeout($time_limit)
->setByteLimit($byte_limit)
->setLinesOfContext(0)
->executeInline();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// TODO: This diff has 3 lines of context, which produces slightly
// incorrect "added file content" and "removed file content" results.
// This may also choke on binaries, but "svnlook diff" does not support
// the "--diff-cmd" flag.
// For subversion, we need to use `svnlook`.
$future = new ExecFuture(
'svnlook diff -t %s %s',
$this->subversionTransaction,
$this->subversionRepository);
$future->setTimeout($time_limit);
$future->setStdoutSizeLimit($byte_limit);
$future->setStderrSizeLimit($byte_limit);
list($raw_diff) = $future->resolvex();
break;
default:
throw new Exception(pht("Unknown VCS '%s!'", $vcs));
}
if (strlen($raw_diff) >= $byte_limit) {
throw new Exception(
pht(
'The raw text of this change ("%s") is enormous (larger than %s '.
'bytes).',
$identifier,
new PhutilNumber($byte_limit)));
}
if (!strlen($raw_diff)) {
// If the commit is actually empty, just return no changesets.
- return array();
+ return array(array(), 0);
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$changes);
- return $diff->getChangesets();
+
+ $changesets = $diff->getChangesets();
+ $size = strlen($raw_diff);
+
+ return array($changesets, $size);
+ }
+
+ public function getChangesetsForCommit($identifier) {
+ if (isset($this->changesets[$identifier])) {
+ $cached = $this->changesets[$identifier];
+
+ if ($cached instanceof Exception) {
+ throw $cached;
+ }
+
+ return $cached;
+ }
+
+ $info = $this->loadChangesetsForCommit($identifier);
+ list($changesets, $size) = $info;
+ return $changesets;
}
public function getChangesetsForCommit($identifier) {
if (isset($this->changesets[$identifier])) {
$cached = $this->changesets[$identifier];
if ($cached instanceof Exception) {
throw $cached;
}
return $cached;
}
return $this->loadChangesetsForCommit($identifier);
}
public function loadCommitRefForCommit($identifier) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return id(new DiffusionLowLevelCommitQuery())
->setRepository($repository)
->withIdentifier($identifier)
->execute();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// For subversion, we need to use `svnlook`.
list($message) = execx(
'svnlook log -t %s %s',
$this->subversionTransaction,
$this->subversionRepository);
return id(new DiffusionCommitRef())
->setMessage($message);
break;
default:
throw new Exception(pht("Unknown VCS '%s!'", $vcs));
}
}
public function loadBranches($identifier) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return idx($this->gitCommits, $identifier, array());
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: This will be "the branch the commit was made to", not
// "a list of all branch heads which descend from the commit".
// This is consistent with Mercurial, but possibly confusing.
return idx($this->mercurialCommits, $identifier, array());
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// Subversion doesn't have branches.
return array();
}
}
private function loadCommitInfoForWorker(array $all_updates) {
$type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
$map = array();
foreach ($all_updates as $update) {
if ($update->getRefType() != $type_commit) {
continue;
}
$map[$update->getRefNew()] = array();
}
foreach ($map as $identifier => $info) {
$ref = $this->loadCommitRefForCommit($identifier);
$map[$identifier] += array(
'summary' => $ref->getSummary(),
'branches' => $this->loadBranches($identifier),
);
}
return $map;
}
private function isInitialImport(array $all_updates) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// There is no meaningful way to import history into Subversion by
// pushing.
return false;
default:
break;
}
// Now, apply a heuristic to guess whether this is a normal commit or
// an initial import. We guess something is an initial import if:
//
// - the repository is currently empty; and
// - it pushes more than 7 commits at once.
//
// The number "7" is chosen arbitrarily as seeming reasonable. We could
// also look at author data (do the commits come from multiple different
// authors?) and commit date data (is the oldest commit more than 48 hours
// old), but we don't have immediate access to those and this simple
// heuristic might be good enough.
$commit_count = 0;
$type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
foreach ($all_updates as $update) {
if ($update->getRefType() != $type_commit) {
continue;
}
$commit_count++;
}
if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) {
// If this pushes a very small number of commits, assume it's an
// initial commit or stack of a few initial commits.
return false;
}
$any_commits = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withRepository($repository)
->setLimit(1)
->execute();
if ($any_commits) {
// If the repository already has commits, this isn't an import.
return false;
}
return true;
}
}
diff --git a/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php b/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php
index bb222df28..748386f1b 100644
--- a/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php
+++ b/src/applications/diffusion/harbormaster/DiffusionBuildableEngine.php
@@ -1,37 +1,43 @@
<?php
final class DiffusionBuildableEngine
extends HarbormasterBuildableEngine {
public function publishBuildable(
HarbormasterBuildable $old,
HarbormasterBuildable $new) {
// Don't publish manual buildables.
if ($new->getIsManualBuildable()) {
return;
}
// Don't publish anything if the buildable status has not changed. At
// least for now, Diffusion handles buildable status exactly the same
// way that Harbormaster does.
$old_status = $old->getBuildableStatus();
$new_status = $new->getBuildableStatus();
if ($old_status === $new_status) {
return;
}
// Don't publish anything if the buildable is still building.
if ($new->isBuilding()) {
return;
}
$xaction = $this->newTransaction()
->setMetadataValue('harbormaster:buildablePHID', $new->getPHID())
->setTransactionType(DiffusionCommitBuildableTransaction::TRANSACTIONTYPE)
->setNewValue($new->getBuildableStatus());
$this->applyTransactions(array($xaction));
}
+ public function getAuthorIdentity() {
+ return $this->getObject()
+ ->loadIdentities($this->getViewer())
+ ->getAuthorIdentity();
+ }
+
}
diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php
index fc3c8c4f5..f3a0ef53a 100644
--- a/src/applications/diffusion/herald/HeraldCommitAdapter.php
+++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php
@@ -1,372 +1,372 @@
<?php
final class HeraldCommitAdapter
extends HeraldAdapter
implements HarbormasterBuildableAdapterInterface {
protected $diff;
protected $revision;
protected $commit;
private $commitDiff;
protected $affectedPaths;
protected $affectedRevision;
protected $affectedPackages;
protected $auditNeededPackages;
private $buildRequests = array();
public function getAdapterApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function newObject() {
return new PhabricatorRepositoryCommit();
}
public function isTestAdapterForObject($object) {
return ($object instanceof PhabricatorRepositoryCommit);
}
public function getAdapterTestDescription() {
return pht(
'Test rules which run after a commit is discovered and imported.');
}
public function newTestAdapter(PhabricatorUser $viewer, $object) {
$object = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($object->getPHID()))
->needCommitData(true)
->executeOne();
if (!$object) {
throw new Exception(
pht(
'Failed to reload commit ("%s") to fetch commit data.',
$object->getPHID()));
}
return id(clone $this)
->setObject($object);
}
protected function initializeNewAdapter() {
$this->commit = $this->newObject();
}
public function setObject($object) {
$this->commit = $object;
return $this;
}
public function getObject() {
return $this->commit;
}
public function getAdapterContentType() {
return 'commit';
}
public function getAdapterContentName() {
return pht('Commits');
}
public function getAdapterContentDescription() {
return pht(
"React to new commits appearing in tracked repositories.\n".
"Commit rules can send email, flag commits, trigger audits, ".
"and run build plans.");
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return true;
default:
return false;
}
}
public function canTriggerOnObject($object) {
if ($object instanceof PhabricatorRepository) {
return true;
}
if ($object instanceof PhabricatorProject) {
return true;
}
return false;
}
public function getTriggerObjectPHIDs() {
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$repository_phid = $this->getRepository()->getPHID();
$commit_phid = $this->getObject()->getPHID();
$phids = array();
$phids[] = $commit_phid;
$phids[] = $repository_phid;
// NOTE: This is projects for the repository, not for the commit. When
// Herald evaluates, commits normally can not have any project tags yet.
$repository_project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$repository_phid,
$project_type);
foreach ($repository_project_phids as $phid) {
$phids[] = $phid;
}
$phids = array_unique($phids);
$phids = array_values($phids);
return $phids;
}
public function explainValidTriggerObjects() {
return pht('This rule can trigger for **repositories** and **projects**.');
}
public function getHeraldName() {
return $this->commit->getMonogram();
}
public function loadAffectedPaths() {
$viewer = $this->getViewer();
if ($this->affectedPaths === null) {
$result = PhabricatorOwnerPathQuery::loadAffectedPaths(
$this->getRepository(),
$this->commit,
$viewer);
$this->affectedPaths = $result;
}
return $this->affectedPaths;
}
public function loadAffectedPackages() {
if ($this->affectedPackages === null) {
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$this->getRepository(),
$this->loadAffectedPaths());
$this->affectedPackages = $packages;
}
return $this->affectedPackages;
}
public function loadAuditNeededPackages() {
if ($this->auditNeededPackages === null) {
$status_arr = array(
PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
PhabricatorAuditStatusConstants::CONCERNED,
);
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s AND auditStatus IN (%Ls)',
$this->commit->getPHID(),
$status_arr);
$this->auditNeededPackages = $requests;
}
return $this->auditNeededPackages;
}
public function loadDifferentialRevision() {
$viewer = $this->getViewer();
if ($this->affectedRevision === null) {
$this->affectedRevision = false;
$commit = $this->getObject();
$data = $commit->getCommitData();
$revision_id = $data->getCommitDetail('differential.revisionID');
if ($revision_id) {
// NOTE: The Herald rule owner might not actually have access to
// the revision, and can control which revision a commit is
// associated with by putting text in the commit message. However,
// the rules they can write against revisions don't actually expose
// anything interesting, so it seems reasonable to load unconditionally
// here.
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($revision_id))
->setViewer($viewer)
->needReviewers(true)
->executeOne();
if ($revision) {
$this->affectedRevision = $revision;
}
}
}
return $this->affectedRevision;
}
public static function getEnormousByteLimit() {
- return 1024 * 1024 * 1024; // 1GB
+ return 256 * 1024 * 1024; // 256MB. See T13142 and T13143.
}
public static function getEnormousTimeLimit() {
return 60 * 15; // 15 Minutes
}
private function loadCommitDiff() {
$viewer = $this->getViewer();
$byte_limit = self::getEnormousByteLimit();
$time_limit = self::getEnormousTimeLimit();
$diff_info = $this->callConduit(
'diffusion.rawdiffquery',
array(
'commit' => $this->commit->getCommitIdentifier(),
'timeout' => $time_limit,
'byteLimit' => $byte_limit,
'linesOfContext' => 0,
));
if ($diff_info['tooHuge']) {
throw new Exception(
pht(
'The raw text of this change is enormous (larger than %s byte(s)). '.
'Herald can not process it.',
new PhutilNumber($byte_limit)));
}
if ($diff_info['tooSlow']) {
throw new Exception(
pht(
'The raw text of this change took too long to process (longer '.
'than %s second(s)). Herald can not process it.',
new PhutilNumber($time_limit)));
}
$file_phid = $diff_info['filePHID'];
$diff_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$diff_file) {
throw new Exception(
pht(
'Failed to load diff ("%s") for this change.',
$file_phid));
}
$raw = $diff_file->loadFileData();
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$changes);
return $diff;
}
public function isDiffEnormous() {
$this->loadDiffContent('*');
return ($this->commitDiff instanceof Exception);
}
public function loadDiffContent($type) {
if ($this->commitDiff === null) {
try {
$this->commitDiff = $this->loadCommitDiff();
} catch (Exception $ex) {
$this->commitDiff = $ex;
phlog($ex);
}
}
if ($this->commitDiff instanceof Exception) {
$ex = $this->commitDiff;
$ex_class = get_class($ex);
$ex_message = pht('Failed to load changes: %s', $ex->getMessage());
return array(
'<'.$ex_class.'>' => $ex_message,
);
}
$changes = $this->commitDiff->getChangesets();
$result = array();
foreach ($changes as $change) {
$lines = array();
foreach ($change->getHunks() as $hunk) {
switch ($type) {
case '-':
$lines[] = $hunk->makeOldFile();
break;
case '+':
$lines[] = $hunk->makeNewFile();
break;
case '*':
$lines[] = $hunk->makeChanges();
break;
default:
throw new Exception(pht("Unknown content selection '%s'!", $type));
}
}
$result[$change->getFilename()] = implode("\n", $lines);
}
return $result;
}
public function loadIsMergeCommit() {
$parents = $this->callConduit(
'diffusion.commitparentsquery',
array(
'commit' => $this->getObject()->getCommitIdentifier(),
));
return (count($parents) > 1);
}
private function callConduit($method, array $params) {
$viewer = $this->getViewer();
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $this->getRepository(),
'commit' => $this->commit->getCommitIdentifier(),
));
return DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
$method,
$params);
}
private function getRepository() {
return $this->getObject()->getRepository();
}
/* -( HarbormasterBuildableAdapterInterface )------------------------------ */
public function getHarbormasterBuildablePHID() {
return $this->getObject()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getObject()->getRepository()->getPHID();
}
public function getQueuedHarbormasterBuildRequests() {
return $this->buildRequests;
}
public function queueHarbormasterBuildRequest(
HarbormasterBuildRequest $request) {
$this->buildRequests[] = $request;
}
}
diff --git a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
index 251935a6d..8a9deb5d8 100644
--- a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
+++ b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
@@ -1,132 +1,137 @@
<?php
final class DiffusionMercurialWireProtocol extends Phobject {
public static function getCommandArgs($command) {
// We need to enumerate all of the Mercurial wire commands because the
// argument encoding varies based on the command. "Why?", you might ask,
// "Why would you do this?".
$commands = array(
'batch' => array('cmds', '*'),
'between' => array('pairs'),
'branchmap' => array(),
'branches' => array('nodes'),
'capabilities' => array(),
'changegroup' => array('roots'),
'changegroupsubset' => array('bases heads'),
'debugwireargs' => array('one two *'),
'getbundle' => array('*'),
'heads' => array(),
'hello' => array(),
'known' => array('nodes', '*'),
'listkeys' => array('namespace'),
'lookup' => array('key'),
'pushkey' => array('namespace', 'key', 'old', 'new'),
+ 'protocaps' => array('caps'),
'stream_out' => array(''),
'unbundle' => array('heads'),
);
if (!isset($commands[$command])) {
- throw new Exception(pht("Unknown Mercurial command '%s!", $command));
+ throw new Exception(
+ pht(
+ 'Unknown Mercurial command "%s"!',
+ $command));
}
return $commands[$command];
}
public static function isReadOnlyCommand($command) {
$read_only = array(
'between' => true,
'branchmap' => true,
'branches' => true,
'capabilities' => true,
'changegroup' => true,
'changegroupsubset' => true,
'debugwireargs' => true,
'getbundle' => true,
'heads' => true,
'hello' => true,
'known' => true,
'listkeys' => true,
'lookup' => true,
+ 'protocaps' => true,
'stream_out' => true,
);
// Notably, the write commands are "pushkey" and "unbundle". The
// "batch" command is theoretically read only, but we require explicit
// analysis of the actual commands.
return isset($read_only[$command]);
}
public static function isReadOnlyBatchCommand($cmds) {
if (!strlen($cmds)) {
// We expect a "batch" command to always have a "cmds" string, so err
// on the side of caution and throw if we don't get any data here. This
// either indicates a mangled command from the client or a programming
// error in our code.
throw new Exception(pht("Expected nonempty '%s' specification!", 'cmds'));
}
// For "batch" we get a "cmds" argument like:
//
// heads ;known nodes=
//
// We need to examine the commands (here, "heads" and "known") to make sure
// they're all read-only.
// NOTE: Mercurial has some code to escape semicolons, but it does not
// actually function for command separation. For example, these two batch
// commands will produce completely different results (the former will run
// the lookup; the latter will fail with a parser error):
//
// lookup key=a:xb;lookup key=z* 0
// lookup key=a:;b;lookup key=z* 0
// ^
// |
// +-- Note semicolon.
//
// So just split unconditionally.
$cmds = explode(';', $cmds);
foreach ($cmds as $sub_cmd) {
$name = head(explode(' ', $sub_cmd, 2));
if (!self::isReadOnlyCommand($name)) {
return false;
}
}
return true;
}
/** If the server version is running 3.4+ it will respond
* with 'bundle2' capability in the format of "bundle2=(url-encoding)".
* Until we manage to properly package up bundles to send back we
* disallow the client from knowing we speak bundle2 by removing it
* from the capabilities listing.
*
* The format of the capabilities string is: "a space separated list
* of strings representing what commands the server supports"
* @link https://www.mercurial-scm.org/wiki/CommandServer#Protocol
*
* @param string $capabilities - The string of capabilities to
* strip the bundle2 capability from. This is expected to be
* the space-separated list of strings resulting from the
* querying the 'capabilities' command.
*
* @return string The resulting space-separated list of capabilities
* which no longer contains the 'bundle2' capability. This is meant
* to replace the original $body to send back to client.
*/
public static function filterBundle2Capability($capabilities) {
$parts = explode(' ', $capabilities);
foreach ($parts as $key => $part) {
if (preg_match('/^bundle2=/', $part)) {
unset($parts[$key]);
break;
}
}
return implode(' ', $parts);
}
}
diff --git a/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php b/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php
index 1cb0f107a..251967f31 100644
--- a/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php
+++ b/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php
@@ -1,197 +1,220 @@
<?php
final class DiffusionSubversionWireProtocol extends Phobject {
private $buffer = '';
private $state = 'item';
private $expectBytes = 0;
private $byteBuffer = '';
private $stack = array();
private $list = array();
private $raw = '';
private function pushList() {
$this->stack[] = $this->list;
$this->list = array();
}
private function popList() {
$list = $this->list;
$this->list = array_pop($this->stack);
return $list;
}
private function pushItem($item, $type) {
$this->list[] = array(
'type' => $type,
'value' => $item,
);
}
public function writeData($data) {
$this->buffer .= $data;
$messages = array();
while (true) {
- if ($this->state == 'item') {
+ if ($this->state == 'space') {
+ // Consume zero or more extra spaces after matching an item. The
+ // protocol requires at least one space, but allows more than one.
+
+ $matches = null;
+ if (!preg_match('/^(\s*)\S/', $this->buffer, $matches)) {
+ // Wait for more data.
+ break;
+ }
+
+ // We have zero or more spaces and then some other character, so throw
+ // the spaces away and continue parsing frames.
+ if (strlen($matches[1])) {
+ $this->buffer = substr($this->buffer, strlen($matches[1]));
+ }
+
+ $this->state = 'item';
+ } else if ($this->state == 'item') {
$match = null;
$result = null;
$buf = $this->buffer;
if (preg_match('/^([a-z][a-z0-9-]*)\s/i', $buf, $match)) {
$this->pushItem($match[1], 'word');
} else if (preg_match('/^(\d+)\s/', $buf, $match)) {
$this->pushItem((int)$match[1], 'number');
} else if (preg_match('/^(\d+):/', $buf, $match)) {
// NOTE: The "+ 1" includes the space after the string.
$this->expectBytes = (int)$match[1] + 1;
$this->state = 'bytes';
} else if (preg_match('/^(\\()\s/', $buf, $match)) {
$this->pushList();
} else if (preg_match('/^(\\))\s/', $buf, $match)) {
$list = $this->popList();
if ($this->stack) {
$this->pushItem($list, 'list');
} else {
$result = $list;
}
} else {
$match = false;
}
if ($match !== false) {
$this->raw .= substr($this->buffer, 0, strlen($match[0]));
$this->buffer = substr($this->buffer, strlen($match[0]));
if ($result !== null) {
$messages[] = array(
'structure' => $list,
'raw' => $this->raw,
);
$this->raw = '';
}
+
+ // Consume any extra whitespace after an item. If we're in the
+ // "bytes" state, we aren't looking for whitespace.
+ if ($this->state == 'item') {
+ $this->state = 'space';
+ }
} else {
// No matches yet, wait for more data.
break;
}
} else if ($this->state == 'bytes') {
$new_data = substr($this->buffer, 0, $this->expectBytes);
if (!strlen($new_data)) {
// No more bytes available yet, wait for more data.
break;
}
$this->buffer = substr($this->buffer, strlen($new_data));
$this->expectBytes -= strlen($new_data);
$this->raw .= $new_data;
$this->byteBuffer .= $new_data;
if (!$this->expectBytes) {
$this->state = 'byte-space';
// Strip off the terminal space.
$this->pushItem(substr($this->byteBuffer, 0, -1), 'string');
$this->byteBuffer = '';
- $this->state = 'item';
+ $this->state = 'space';
}
} else {
throw new Exception(pht("Invalid state '%s'!", $this->state));
}
}
return $messages;
}
/**
* Convert a parsed command struct into a wire protocol string.
*/
public function serializeStruct(array $struct) {
$out = array();
$out[] = '( ';
foreach ($struct as $item) {
$value = $item['value'];
$type = $item['type'];
switch ($type) {
case 'word':
$out[] = $value;
break;
case 'number':
$out[] = $value;
break;
case 'string':
$out[] = strlen($value).':'.$value;
break;
case 'list':
$out[] = self::serializeStruct($value);
break;
default:
throw new Exception(
pht(
"Unknown SVN wire protocol structure '%s'!",
$type));
}
if ($type != 'list') {
$out[] = ' ';
}
}
$out[] = ') ';
return implode('', $out);
}
public function isReadOnlyCommand(array $struct) {
if (empty($struct[0]['type']) || ($struct[0]['type'] != 'word')) {
// This isn't what we expect; fail defensively.
throw new Exception(
pht(
"Unexpected command structure, expected '%s'.",
'( word ... )'));
}
switch ($struct[0]['value']) {
// Authentication command set.
case 'EXTERNAL':
// The "Main" command set. Some of the commands in this command set are
// mutation commands, and are omitted from this list.
case 'reparent':
case 'get-latest-rev':
case 'get-dated-rev':
case 'rev-proplist':
case 'rev-prop':
case 'get-file':
case 'get-dir':
case 'check-path':
case 'stat':
case 'update':
case 'get-mergeinfo':
case 'switch':
case 'status':
case 'diff':
case 'log':
case 'get-file-revs':
case 'get-locations':
// The "Report" command set. These are not actually mutation
// operations, they just define a request for information.
case 'set-path':
case 'delete-path':
case 'link-path':
case 'finish-report':
case 'abort-report':
// These are used to report command results.
case 'success':
case 'failure':
// If we get here, we've matched some known read-only command.
return true;
default:
// Anything else isn't a known read-only command, so require write
// access to use it.
break;
}
return false;
}
}
diff --git a/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php b/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php
index bd99b8203..f4309b0e7 100644
--- a/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php
+++ b/src/applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php
@@ -1,105 +1,160 @@
<?php
final class DiffusionSubversionWireProtocolTestCase
extends PhabricatorTestCase {
public function testSubversionWireProtocolParser() {
$this->assertSameSubversionMessages(
'( ) ',
array(
array(
),
));
$this->assertSameSubversionMessages(
'( duck 5:quack 42 ( item1 item2 ) ) ',
array(
array(
array(
'type' => 'word',
'value' => 'duck',
),
array(
'type' => 'string',
'value' => 'quack',
),
array(
'type' => 'number',
'value' => 42,
),
array(
'type' => 'list',
'value' => array(
array(
'type' => 'word',
'value' => 'item1',
),
array(
'type' => 'word',
'value' => 'item2',
),
),
),
),
));
$this->assertSameSubversionMessages(
'( msg1 ) ( msg2 ) ',
array(
array(
array(
'type' => 'word',
'value' => 'msg1',
),
),
array(
array(
'type' => 'word',
'value' => 'msg2',
),
),
));
+
+ // This is testing that multiple spaces are parsed correctly. See T13140
+ // for discussion.
+ $this->assertSameSubversionMessages(
+ '( get-file true false ) ',
+ // ^-- Note extra space!
+ array(
+ array(
+ array(
+ 'type' => 'word',
+ 'value' => 'get-file',
+ ),
+ array(
+ 'type' => 'word',
+ 'value' => 'true',
+ ),
+ array(
+ 'type' => 'word',
+ 'value' => 'false',
+ ),
+ ),
+ ),
+ '( get-file true false ) ');
+
+ $this->assertSameSubversionMessages(
+ '( duck 5:quack moo ) ',
+ array(
+ array(
+ array(
+ 'type' => 'word',
+ 'value' => 'duck',
+ ),
+ array(
+ 'type' => 'string',
+ 'value' => 'quack',
+ ),
+ array(
+ 'type' => 'word',
+ 'value' => 'moo',
+ ),
+ ),
+ ),
+ '( duck 5:quack moo ) ');
+
}
public function testSubversionWireProtocolPartialFrame() {
$proto = new DiffusionSubversionWireProtocol();
// This is primarily a test that we don't hang when we write() a frame
// which straddles a string boundary.
$msg1 = $proto->writeData('( duck 5:qu');
$msg2 = $proto->writeData('ack ) ');
$this->assertEqual(array(), ipull($msg1, 'structure'));
$this->assertEqual(
array(
array(
array(
'type' => 'word',
'value' => 'duck',
),
array(
'type' => 'string',
'value' => 'quack',
),
),
),
ipull($msg2, 'structure'));
}
- private function assertSameSubversionMessages($string, array $structs) {
+ private function assertSameSubversionMessages(
+ $string,
+ array $structs,
+ $serial_string = null) {
+
$proto = new DiffusionSubversionWireProtocol();
// Verify that the wire message parses into the structs.
$messages = $proto->writeData($string);
$messages = ipull($messages, 'structure');
$this->assertEqual($structs, $messages, 'parse<'.$string.'>');
// Verify that the structs serialize into the wire message.
$serial = array();
foreach ($structs as $struct) {
$serial[] = $proto->serializeStruct($struct);
}
$serial = implode('', $serial);
- $this->assertEqual($string, $serial, 'serialize<'.$string.'>');
+
+ if ($serial_string === null) {
+ $expect_serial = $string;
+ } else {
+ $expect_serial = $serial_string;
+ }
+
+ $this->assertEqual($expect_serial, $serial, 'serialize<'.$string.'>');
}
}
diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php
index 8996bab62..0ca9cd97d 100644
--- a/src/applications/diffusion/query/DiffusionCommitQuery.php
+++ b/src/applications/diffusion/query/DiffusionCommitQuery.php
@@ -1,679 +1,849 @@
<?php
final class DiffusionCommitQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $defaultRepository;
private $identifiers;
private $repositoryIDs;
private $repositoryPHIDs;
private $identifierMap;
private $responsiblePHIDs;
private $statuses;
private $packagePHIDs;
private $unreachable;
private $needAuditRequests;
private $auditIDs;
private $auditorPHIDs;
private $epochMin;
private $epochMax;
private $importing;
+ private $ancestorsOf;
private $needCommitData;
private $needDrafts;
+ private $needIdentities;
+
+ private $mustFilterRefs = false;
+ private $refRepository;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
/**
* Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234",
* or "a9caf12". When an identifier matches multiple commits, they will all
* be returned; callers should be prepared to deal with more results than
* they queried for.
*/
public function withIdentifiers(array $identifiers) {
// Some workflows (like blame lookups) can pass in large numbers of
// duplicate identifiers. We only care about unique identifiers, so
// get rid of duplicates immediately.
$identifiers = array_fuse($identifiers);
$this->identifiers = $identifiers;
return $this;
}
/**
* Look up commits in a specific repository. This is a shorthand for calling
* @{method:withDefaultRepository} and @{method:withRepositoryIDs}.
*/
public function withRepository(PhabricatorRepository $repository) {
$this->withDefaultRepository($repository);
$this->withRepositoryIDs(array($repository->getID()));
return $this;
}
/**
* Look up commits in a specific repository. Prefer
* @{method:withRepositoryIDs}; the underlying table is keyed by ID such
* that this method requires a separate initial query to map PHID to ID.
*/
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
/**
* If a default repository is provided, ambiguous commit identifiers will
* be assumed to belong to the default repository.
*
* For example, "r123" appearing in a commit message in repository X is
* likely to be unambiguously "rX123". Normally the reference would be
* considered ambiguous, but if you provide a default repository it will
* be correctly resolved.
*/
public function withDefaultRepository(PhabricatorRepository $repository) {
$this->defaultRepository = $repository;
return $this;
}
public function withRepositoryIDs(array $repository_ids) {
- $this->repositoryIDs = $repository_ids;
+ $this->repositoryIDs = array_unique($repository_ids);
return $this;
}
public function needCommitData($need) {
$this->needCommitData = $need;
return $this;
}
public function needDrafts($need) {
$this->needDrafts = $need;
return $this;
}
+ public function needIdentities($need) {
+ $this->needIdentities = $need;
+ return $this;
+ }
+
public function needAuditRequests($need) {
$this->needAuditRequests = $need;
return $this;
}
public function withAuditIDs(array $ids) {
$this->auditIDs = $ids;
return $this;
}
public function withAuditorPHIDs(array $auditor_phids) {
$this->auditorPHIDs = $auditor_phids;
return $this;
}
public function withResponsiblePHIDs(array $responsible_phids) {
$this->responsiblePHIDs = $responsible_phids;
return $this;
}
public function withPackagePHIDs(array $package_phids) {
$this->packagePHIDs = $package_phids;
return $this;
}
public function withUnreachable($unreachable) {
$this->unreachable = $unreachable;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withEpochRange($min, $max) {
$this->epochMin = $min;
$this->epochMax = $max;
return $this;
}
public function withImporting($importing) {
$this->importing = $importing;
return $this;
}
+ public function withAncestorsOf(array $refs) {
+ $this->ancestorsOf = $refs;
+ return $this;
+ }
+
public function getIdentifierMap() {
if ($this->identifierMap === null) {
throw new Exception(
pht(
'You must %s the query before accessing the identifier map.',
'execute()'));
}
return $this->identifierMap;
}
protected function getPrimaryTableAlias() {
return 'commit';
}
protected function willExecute() {
if ($this->identifierMap === null) {
$this->identifierMap = array();
}
}
public function newResultObject() {
return new PhabricatorRepositoryCommit();
}
protected function loadPage() {
$table = $this->newResultObject();
$conn = $table->establishConnection('r');
$subqueries = array();
if ($this->responsiblePHIDs) {
$base_authors = $this->authorPHIDs;
$base_auditors = $this->auditorPHIDs;
$responsible_phids = $this->responsiblePHIDs;
if ($base_authors) {
$all_authors = array_merge($base_authors, $responsible_phids);
} else {
$all_authors = $responsible_phids;
}
if ($base_auditors) {
$all_auditors = array_merge($base_auditors, $responsible_phids);
} else {
$all_auditors = $responsible_phids;
}
$this->authorPHIDs = $all_authors;
$this->auditorPHIDs = $base_auditors;
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
$this->authorPHIDs = $base_authors;
$this->auditorPHIDs = $all_auditors;
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
} else {
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
}
if (count($subqueries) > 1) {
foreach ($subqueries as $key => $subquery) {
$subqueries[$key] = '('.$subquery.')';
}
$query = qsprintf(
$conn,
'%Q %Q %Q',
implode(' UNION DISTINCT ', $subqueries),
$this->buildOrderClause($conn, true),
$this->buildLimitClause($conn));
} else {
$query = head($subqueries);
}
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $commits) {
$repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIDs($repository_ids)
->execute();
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$result = array();
foreach ($commits as $key => $commit) {
$repo = idx($repos, $commit->getRepositoryID());
if ($repo) {
$commit->attachRepository($repo);
} else {
$this->didRejectResult($commit);
unset($commits[$key]);
continue;
}
// Build the identifierMap
if ($this->identifiers !== null) {
$ids = $this->identifiers;
$prefixes = array(
'r'.$commit->getRepository()->getCallsign(),
'r'.$commit->getRepository()->getCallsign().':',
'R'.$commit->getRepository()->getID().':',
'', // No prefix is valid too and will only match the commitIdentifier
);
$suffix = $commit->getCommitIdentifier();
if ($commit->getRepository()->isSVN()) {
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$suffix])) {
$result[$prefix.$suffix][] = $commit;
}
}
} else {
// This awkward construction is so we can link the commits up in O(N)
// time instead of O(N^2).
for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) {
$part = substr($suffix, 0, $ii);
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$part])) {
$result[$prefix.$part][] = $commit;
}
}
}
}
}
}
if ($result) {
foreach ($result as $identifier => $matching_commits) {
if (count($matching_commits) == 1) {
$result[$identifier] = head($matching_commits);
} else {
// This reference is ambiguous (it matches more than one commit) so
// don't link it.
unset($result[$identifier]);
}
}
$this->identifierMap += $result;
}
return $commits;
}
protected function didFilterPage(array $commits) {
$viewer = $this->getViewer();
+ if ($this->mustFilterRefs) {
+ // If this flag is set, the query has an "Ancestors Of" constraint and
+ // at least one of the constraining refs had too many ancestors for us
+ // to apply the constraint with a big "commitIdentifier IN (%Ls)" clause.
+ // We're going to filter each page and hope we get a full result set
+ // before the query overheats.
+
+ $ancestor_list = mpull($commits, 'getCommitIdentifier');
+ $ancestor_list = array_values($ancestor_list);
+
+ foreach ($this->ancestorsOf as $ref) {
+ try {
+ $ancestor_list = DiffusionQuery::callConduitWithDiffusionRequest(
+ $viewer,
+ DiffusionRequest::newFromDictionary(
+ array(
+ 'repository' => $this->refRepository,
+ 'user' => $viewer,
+ )),
+ 'diffusion.internal.ancestors',
+ array(
+ 'ref' => $ref,
+ 'commits' => $ancestor_list,
+ ));
+ } catch (ConduitClientException $ex) {
+ throw new PhabricatorSearchConstraintException(
+ $ex->getMessage());
+ }
+
+ if (!$ancestor_list) {
+ break;
+ }
+ }
+
+ $ancestor_list = array_fuse($ancestor_list);
+ foreach ($commits as $key => $commit) {
+ $identifier = $commit->getCommitIdentifier();
+ if (!isset($ancestor_list[$identifier])) {
+ $this->didRejectResult($commit);
+ unset($commits[$key]);
+ }
+ }
+
+ if (!$commits) {
+ return $commits;
+ }
+ }
+
if ($this->needCommitData) {
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $commit) {
$commit_data = idx($data, $commit->getID());
if (!$commit_data) {
$commit_data = new PhabricatorRepositoryCommitData();
}
$commit->attachCommitData($commit_data);
}
}
if ($this->needAuditRequests) {
$requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
'commitPHID IN (%Ls)',
mpull($commits, 'getPHID'));
$requests = mgroup($requests, 'getCommitPHID');
foreach ($commits as $commit) {
$audit_requests = idx($requests, $commit->getPHID(), array());
$commit->attachAudits($audit_requests);
foreach ($audit_requests as $audit_request) {
$audit_request->attachCommit($commit);
}
}
}
+ if ($this->needIdentities) {
+ $identity_phids = array_merge(
+ mpull($commits, 'getAuthorIdentityPHID'),
+ mpull($commits, 'getCommitterIdentityPHID'));
+
+ $data = id(new PhabricatorRepositoryIdentityQuery())
+ ->withPHIDs($identity_phids)
+ ->setViewer($this->getViewer())
+ ->execute();
+ $data = mpull($data, null, 'getPHID');
+
+ foreach ($commits as $commit) {
+ $author_identity = idx($data, $commit->getAuthorIdentityPHID());
+ $committer_identity = idx($data, $commit->getCommitterIdentityPHID());
+ $commit->attachIdentities($author_identity, $committer_identity);
+ }
+ }
+
if ($this->needDrafts) {
PhabricatorDraftEngine::attachDrafts(
$viewer,
$commits);
}
return $commits;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->repositoryPHIDs !== null) {
$map_repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($this->repositoryPHIDs)
->execute();
if (!$map_repositories) {
throw new PhabricatorEmptyQueryException();
}
$repository_ids = mpull($map_repositories, 'getID');
if ($this->repositoryIDs !== null) {
$repository_ids = array_merge($repository_ids, $this->repositoryIDs);
}
$this->withRepositoryIDs($repository_ids);
}
+ if ($this->ancestorsOf !== null) {
+ if (count($this->repositoryIDs) !== 1) {
+ throw new PhabricatorSearchConstraintException(
+ pht(
+ 'To search for commits which are ancestors of particular refs, '.
+ 'you must constrain the search to exactly one repository.'));
+ }
+
+ $repository_id = head($this->repositoryIDs);
+ $history_limit = $this->getRawResultLimit() * 32;
+ $viewer = $this->getViewer();
+
+ $repository = id(new PhabricatorRepositoryQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($repository_id))
+ ->executeOne();
+
+ if (!$repository) {
+ throw new PhabricatorEmptyQueryException();
+ }
+
+ if ($repository->isSVN()) {
+ throw new PhabricatorSearchConstraintException(
+ pht(
+ 'Subversion does not support searching for ancestors of '.
+ 'a particular ref. This operation is not meaningful in '.
+ 'Subversion.'));
+ }
+
+ if ($repository->isHg()) {
+ throw new PhabricatorSearchConstraintException(
+ pht(
+ 'Mercurial does not currently support searching for ancestors of '.
+ 'a particular ref.'));
+ }
+
+ $can_constrain = true;
+ $history_identifiers = array();
+ foreach ($this->ancestorsOf as $key => $ref) {
+ try {
+ $raw_history = DiffusionQuery::callConduitWithDiffusionRequest(
+ $viewer,
+ DiffusionRequest::newFromDictionary(
+ array(
+ 'repository' => $repository,
+ 'user' => $viewer,
+ )),
+ 'diffusion.historyquery',
+ array(
+ 'commit' => $ref,
+ 'limit' => $history_limit,
+ ));
+ } catch (ConduitClientException $ex) {
+ throw new PhabricatorSearchConstraintException(
+ $ex->getMessage());
+ }
+
+ $ref_identifiers = array();
+ foreach ($raw_history['pathChanges'] as $change) {
+ $ref_identifiers[] = $change['commitIdentifier'];
+ }
+
+ // If this ref had fewer total commits than the limit, we're safe to
+ // apply the constraint as a large `IN (...)` query for a list of
+ // commit identifiers. This is efficient.
+ if ($history_limit) {
+ if (count($ref_identifiers) >= $history_limit) {
+ $can_constrain = false;
+ break;
+ }
+ }
+
+ $history_identifiers += array_fuse($ref_identifiers);
+ }
+
+ // If all refs had a small number of ancestors, we can just put the
+ // constraint into the query here and we're done. Otherwise, we need
+ // to filter each page after it comes out of the MySQL layer.
+ if ($can_constrain) {
+ $where[] = qsprintf(
+ $conn,
+ 'commit.commitIdentifier IN (%Ls)',
+ $history_identifiers);
+ } else {
+ $this->mustFilterRefs = true;
+ $this->refRepository = $repository;
+ }
+ }
+
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'commit.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'commit.phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryIDs !== null) {
$where[] = qsprintf(
$conn,
'commit.repositoryID IN (%Ld)',
$this->repositoryIDs);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'commit.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->epochMin !== null) {
$where[] = qsprintf(
$conn,
'commit.epoch >= %d',
$this->epochMin);
}
if ($this->epochMax !== null) {
$where[] = qsprintf(
$conn,
'commit.epoch <= %d',
$this->epochMax);
}
if ($this->importing !== null) {
if ($this->importing) {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) != %d',
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
} else {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = %d',
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
}
}
if ($this->identifiers !== null) {
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$refs = array();
$bare = array();
foreach ($this->identifiers as $identifier) {
$matches = null;
preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/',
$identifier, $matches);
$repo = nonempty(rtrim($matches[1], ':'), null);
$commit_identifier = nonempty($matches[2], null);
if ($repo === null) {
if ($this->defaultRepository) {
$repo = $this->defaultRepository->getPHID();
}
}
if ($repo === null) {
if (strlen($commit_identifier) < $min_unqualified) {
continue;
}
$bare[] = $commit_identifier;
} else {
$refs[] = array(
'repository' => $repo,
'identifier' => $commit_identifier,
);
}
}
$sql = array();
foreach ($bare as $identifier) {
$sql[] = qsprintf(
$conn,
'(commit.commitIdentifier LIKE %> AND '.
'LENGTH(commit.commitIdentifier) = 40)',
$identifier);
}
if ($refs) {
$repositories = ipull($refs, 'repository');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIdentifiers($repositories);
$repos->execute();
$repos = $repos->getIdentifierMap();
foreach ($refs as $key => $ref) {
$repo = idx($repos, $ref['repository']);
if (!$repo) {
continue;
}
if ($repo->isSVN()) {
if (!ctype_digit((string)$ref['identifier'])) {
continue;
}
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d AND commit.commitIdentifier = %s)',
$repo->getID(),
// NOTE: Because the 'commitIdentifier' column is a string, MySQL
// ignores the index if we hand it an integer. Hand it a string.
// See T3377.
(int)$ref['identifier']);
} else {
if (strlen($ref['identifier']) < $min_qualified) {
continue;
}
$identifier = $ref['identifier'];
if (strlen($identifier) == 40) {
// MySQL seems to do slightly better with this version if the
// clause, so issue it if we have a full commit hash.
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d
AND commit.commitIdentifier = %s)',
$repo->getID(),
$identifier);
} else {
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d
AND commit.commitIdentifier LIKE %>)',
$repo->getID(),
$identifier);
}
}
}
}
if (!$sql) {
// If we discarded all possible identifiers (e.g., they all referenced
// bogus repositories or were all too short), make sure the query finds
// nothing.
throw new PhabricatorEmptyQueryException(
pht('No commit identifiers.'));
}
$where[] = '('.implode(' OR ', $sql).')';
}
if ($this->auditIDs !== null) {
$where[] = qsprintf(
$conn,
'auditor.id IN (%Ld)',
$this->auditIDs);
}
if ($this->auditorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'auditor.auditorPHID IN (%Ls)',
$this->auditorPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'commit.auditStatus IN (%Ld)',
$this->statuses);
}
if ($this->packagePHIDs !== null) {
$where[] = qsprintf(
$conn,
'package.dst IN (%Ls)',
$this->packagePHIDs);
}
if ($this->unreachable !== null) {
if ($this->unreachable) {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = %d',
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE,
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
} else {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = 0',
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
}
}
return $where;
}
protected function didFilterResults(array $filtered) {
if ($this->identifierMap) {
foreach ($this->identifierMap as $name => $commit) {
if (isset($filtered[$commit->getPHID()])) {
unset($this->identifierMap[$name]);
}
}
}
}
private function shouldJoinAuditor() {
return ($this->auditIDs || $this->auditorPHIDs);
}
private function shouldJoinOwners() {
return (bool)$this->packagePHIDs;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$join = parent::buildJoinClauseParts($conn);
$audit_request = new PhabricatorRepositoryAuditRequest();
if ($this->shouldJoinAuditor()) {
$join[] = qsprintf(
$conn,
'JOIN %T auditor ON commit.phid = auditor.commitPHID',
$audit_request->getTableName());
}
if ($this->shouldJoinOwners()) {
$join[] = qsprintf(
$conn,
'JOIN %T package ON commit.phid = package.src
AND package.type = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
DiffusionCommitHasPackageEdgeType::EDGECONST);
}
return $join;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinAuditor()) {
return true;
}
if ($this->shouldJoinOwners()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'epoch' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'epoch',
'type' => 'int',
'reverse' => false,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$commit = $this->loadCursorObject($cursor);
return array(
'id' => $commit->getID(),
'epoch' => $commit->getEpoch(),
);
}
public function getBuiltinOrders() {
$parent = parent::getBuiltinOrders();
// Rename the default ID-based orders.
$parent['importnew'] = array(
'name' => pht('Import Date (Newest First)'),
) + $parent['newest'];
$parent['importold'] = array(
'name' => pht('Import Date (Oldest First)'),
) + $parent['oldest'];
return array(
'newest' => array(
'vector' => array('epoch', 'id'),
'name' => pht('Commit Date (Newest First)'),
),
'oldest' => array(
'vector' => array('-epoch', '-id'),
'name' => pht('Commit Date (Oldest First)'),
),
) + $parent;
}
}
diff --git a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php
new file mode 100644
index 000000000..4d10ee44e
--- /dev/null
+++ b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php
@@ -0,0 +1,108 @@
+<?php
+
+final class DiffusionRepositoryIdentitySearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Repository Identities');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorDiffusionApplication';
+ }
+
+ public function newQuery() {
+ return new PhabricatorRepositoryIdentityQuery();
+ }
+
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new DiffusionIdentityAssigneeSearchField())
+ ->setLabel(pht('Assigned To'))
+ ->setKey('assignee')
+ ->setDescription(pht('Search for identities by assignee.')),
+ id(new PhabricatorSearchTextField())
+ ->setLabel(pht('Identity Contains'))
+ ->setKey('match')
+ ->setDescription(pht('Search for identities by substring.')),
+ id(new PhabricatorSearchThreeStateField())
+ ->setLabel(pht('Is Assigned'))
+ ->setKey('hasEffectivePHID')
+ ->setOptions(
+ pht('(Show All)'),
+ pht('Show Only Assigned Identities'),
+ pht('Show Only Unassigned Identities')),
+ );
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ if ($map['hasEffectivePHID'] !== null) {
+ $query->withHasEffectivePHID($map['hasEffectivePHID']);
+ }
+
+ if ($map['match'] !== null) {
+ $query->withIdentityNameLike($map['match']);
+ }
+
+ if ($map['assignee']) {
+ $query->withAssigneePHIDs($map['assignee']);
+ }
+
+ return $query;
+ }
+
+ protected function getURI($path) {
+ return '/diffusion/identity/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array(
+ 'all' => pht('All Identities'),
+ );
+
+ return $names;
+ }
+
+ public function buildSavedQueryFromBuiltin($query_key) {
+
+ $query = $this->newSavedQuery();
+ $query->setQueryKey($query_key);
+
+ switch ($query_key) {
+ case 'all':
+ return $query;
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+ protected function renderResultList(
+ array $identities,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+ assert_instances_of($identities, 'PhabricatorRepositoryIdentity');
+
+ $viewer = $this->requireViewer();
+
+ $list = new PHUIObjectItemListView();
+ $list->setUser($viewer);
+ foreach ($identities as $identity) {
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName(pht('Identity %d', $identity->getID()))
+ ->setHeader($identity->getIdentityShortName())
+ ->setHref($identity->getURI())
+ ->setObject($identity);
+
+ $list->addItem($item);
+ }
+
+ $result = new PhabricatorApplicationSearchResultView();
+ $result->setObjectList($list);
+ $result->setNoDataString(pht('No Identities found.'));
+
+ return $result;
+ }
+
+}
diff --git a/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php b/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php
index f98cc645a..0eb15cd01 100644
--- a/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php
+++ b/src/applications/diffusion/query/blame/DiffusionGitBlameQuery.php
@@ -1,34 +1,38 @@
<?php
final class DiffusionGitBlameQuery extends DiffusionBlameQuery {
protected function newBlameFuture(DiffusionRequest $request, $path) {
$repository = $request->getRepository();
$commit = $request->getCommit();
+ // NOTE: The "--root" flag suppresses the addition of the "^" boundary
+ // commit marker. Without it, root commits render with a "^" before them,
+ // and one fewer character of the commit hash.
+
return $repository->getLocalCommandFuture(
- '--no-pager blame -s -l %s -- %s',
+ '--no-pager blame --root -s -l %s -- %s',
$commit,
$path);
}
protected function resolveBlameFuture(ExecFuture $future) {
list($err, $stdout) = $future->resolve();
if ($err) {
return null;
}
$result = array();
$lines = phutil_split_lines($stdout);
foreach ($lines as $line) {
list($commit) = explode(' ', $line, 2);
$result[] = $commit;
}
return $result;
}
}
diff --git a/src/applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php b/src/applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php
new file mode 100644
index 000000000..b78e7596e
--- /dev/null
+++ b/src/applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DiffusionIdentityAssigneeSearchField
+ extends PhabricatorSearchTokenizerField {
+
+ protected function getDefaultValue() {
+ return array();
+ }
+
+ protected function getValueFromRequest(AphrontRequest $request, $key) {
+ return $this->getUsersFromRequest($request, $key);
+ }
+
+ protected function newDatasource() {
+ return new DiffusionIdentityAssigneeDatasource();
+ }
+
+ protected function newConduitParameterType() {
+ return new ConduitUserListParameterType();
+ }
+
+}
diff --git a/src/applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php b/src/applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php
new file mode 100644
index 000000000..a9b21c638
--- /dev/null
+++ b/src/applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php
@@ -0,0 +1,21 @@
+<?php
+
+final class DiffusionIdentityAssigneeDatasource
+ extends PhabricatorTypeaheadCompositeDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Assignee');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type a username or function...');
+ }
+
+ public function getComponentDatasources() {
+ return array(
+ new PhabricatorPeopleDatasource(),
+ new DiffusionIdentityUnassignedDatasource(),
+ );
+ }
+
+}
diff --git a/src/applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php b/src/applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php
new file mode 100644
index 000000000..b69498931
--- /dev/null
+++ b/src/applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php
@@ -0,0 +1,77 @@
+<?php
+
+final class DiffusionIdentityUnassignedDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ const FUNCTION_TOKEN = 'unassigned()';
+
+ public function getBrowseTitle() {
+ return pht('Browse Explicitly Unassigned');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type "unassigned"...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorDiffusionApplication';
+ }
+
+ public function getDatasourceFunctions() {
+ return array(
+ 'unassigned' => array(
+ 'name' => pht('Explicitly Unassigned'),
+ 'summary' => pht('Find results which are not assigned.'),
+ 'description' => pht(
+ "This function includes results which have been explicitly ".
+ "unassigned. Use a query like this to find explicitly ".
+ "unassigned results:\n\n%s\n\n".
+ "If you combine this function with other functions, the query will ".
+ "return results which match the other selectors //or// have no ".
+ "assignee. For example, this query will find results which are ".
+ "assigned to `alincoln`, and will also find results which have been ".
+ "unassigned:\n\n%s",
+ '> unassigned()',
+ '> alincoln, unassigned()'),
+ ),
+ );
+ }
+
+ public function loadResults() {
+ $results = array(
+ $this->buildUnassignedResult(),
+ );
+ return $this->filterResultsAgainstTokens($results);
+ }
+
+ protected function evaluateFunction($function, array $argv_list) {
+ $results = array();
+
+ foreach ($argv_list as $argv) {
+ $results[] = self::FUNCTION_TOKEN;
+ }
+
+ return $results;
+ }
+
+ public function renderFunctionTokens($function, array $argv_list) {
+ $results = array();
+ foreach ($argv_list as $argv) {
+ $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
+ $this->buildUnassignedResult());
+ }
+ return $results;
+ }
+
+ private function buildUnassignedResult() {
+ $name = pht('Unassigned');
+ return $this->newFunctionResult()
+ ->setName($name.' unassigned')
+ ->setDisplayName($name)
+ ->setIcon('fa-ban')
+ ->setPHID('unassigned()')
+ ->setUnique(true)
+ ->addAttribute(pht('Select results with no owner.'));
+ }
+
+}
diff --git a/src/applications/diffusion/view/DiffusionHistoryTableView.php b/src/applications/diffusion/view/DiffusionHistoryTableView.php
index 3885bbf47..cfd701967 100644
--- a/src/applications/diffusion/view/DiffusionHistoryTableView.php
+++ b/src/applications/diffusion/view/DiffusionHistoryTableView.php
@@ -1,196 +1,196 @@
<?php
final class DiffusionHistoryTableView extends DiffusionHistoryView {
public function render() {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getUser();
$buildables = $this->loadBuildables(
mpull($this->getHistory(), 'getCommit'));
$has_any_build = false;
$show_revisions = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDifferentialApplication',
$viewer);
$handles = $viewer->loadHandles($this->getRequiredHandlePHIDs());
$graph = null;
if ($this->getParents()) {
$parents = $this->getParents();
// If we're filtering parents, remove relationships which point to
// commits that are not part of the visible graph. Otherwise, we get
// a big tree of nonsense when viewing release branches like "stable"
// versus "master".
if ($this->getFilterParents()) {
foreach ($parents as $key => $nodes) {
foreach ($nodes as $nkey => $node) {
if (empty($parents[$node])) {
unset($parents[$key][$nkey]);
}
}
}
}
$graph = id(new PHUIDiffGraphView())
->setIsHead($this->getIsHead())
->setIsTail($this->getIsTail())
->renderGraph($parents);
}
$show_builds = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorHarbormasterApplication',
$this->getUser());
$rows = array();
$ii = 0;
foreach ($this->getHistory() as $history) {
$epoch = $history->getEpoch();
if ($epoch) {
$committed = $viewer->formatShortDateTime($epoch);
} else {
$committed = null;
}
$data = $history->getCommitData();
$author_phid = $committer = $committer_phid = null;
if ($data) {
$author_phid = $data->getCommitDetail('authorPHID');
$committer_phid = $data->getCommitDetail('committerPHID');
$committer = $data->getCommitDetail('committer');
}
if ($author_phid && isset($handles[$author_phid])) {
$author = $handles[$author_phid]->renderLink();
} else {
$author = self::renderName($history->getAuthorName());
}
$different_committer = false;
if ($committer_phid) {
$different_committer = ($committer_phid != $author_phid);
} else if ($committer != '') {
$different_committer = ($committer != $history->getAuthorName());
}
if ($different_committer) {
if ($committer_phid && isset($handles[$committer_phid])) {
$committer = $handles[$committer_phid]->renderLink();
} else {
$committer = self::renderName($committer);
}
$author = hsprintf('%s/%s', $author, $committer);
}
// We can show details once the message and change have been imported.
$partial_import = PhabricatorRepositoryCommit::IMPORTED_MESSAGE |
PhabricatorRepositoryCommit::IMPORTED_CHANGE;
$commit = $history->getCommit();
if ($commit && $commit->isPartiallyImported($partial_import) && $data) {
$summary = AphrontTableView::renderSingleDisplayLine(
$history->getSummary());
} else {
$summary = phutil_tag('em', array(), pht("Importing\xE2\x80\xA6"));
}
$build = null;
if ($show_builds) {
$buildable = idx($buildables, $commit->getPHID());
if ($buildable !== null) {
$build = $this->renderBuildable($buildable);
$has_any_build = true;
}
}
$browse = $this->linkBrowse(
$history->getPath(),
array(
'commit' => $history->getCommitIdentifier(),
'branch' => $drequest->getBranch(),
'type' => $history->getFileType(),
));
- $status = $commit->getAuditStatus();
- $icon = PhabricatorAuditCommitStatusConstants::getStatusIcon($status);
- $color = PhabricatorAuditCommitStatusConstants::getStatusColor($status);
- $name = PhabricatorAuditCommitStatusConstants::getStatusName($status);
+ $status = $commit->getAuditStatusObject();
+ $icon = $status->getIcon();
+ $color = $status->getColor();
+ $name = $status->getName();
$audit_view = id(new PHUIIconView())
->setIcon($icon, $color)
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $name,
));
$rows[] = array(
$graph ? $graph[$ii++] : null,
$browse,
self::linkCommit(
$drequest->getRepository(),
$history->getCommitIdentifier()),
$build,
$audit_view,
($commit ?
self::linkRevision(idx($this->getRevisions(), $commit->getPHID())) :
null),
$author,
$summary,
$committed,
);
}
$view = new AphrontTableView($rows);
$view->setHeaders(
array(
null,
null,
pht('Commit'),
null,
null,
null,
pht('Author'),
pht('Details'),
pht('Committed'),
));
$view->setColumnClasses(
array(
'threads',
'nudgeright',
'',
'icon',
'icon',
'',
'',
'wide',
'right',
));
$view->setColumnVisibility(
array(
$graph ? true : false,
true,
true,
$has_any_build,
true,
$show_revisions,
));
$view->setDeviceVisibility(
array(
$graph ? true : false,
true,
true,
true,
true,
true,
false,
true,
false,
));
return $view->render();
}
}
diff --git a/src/applications/diffusion/view/DiffusionPushLogListView.php b/src/applications/diffusion/view/DiffusionPushLogListView.php
index e10162575..12a5fa549 100644
--- a/src/applications/diffusion/view/DiffusionPushLogListView.php
+++ b/src/applications/diffusion/view/DiffusionPushLogListView.php
@@ -1,160 +1,173 @@
<?php
final class DiffusionPushLogListView extends AphrontView {
private $logs;
public function setLogs(array $logs) {
assert_instances_of($logs, 'PhabricatorRepositoryPushLog');
$this->logs = $logs;
return $this;
}
public function render() {
$logs = $this->logs;
$viewer = $this->getViewer();
+ $reject_herald = PhabricatorRepositoryPushLog::REJECT_HERALD;
+
$handle_phids = array();
foreach ($logs as $log) {
$handle_phids[] = $log->getPusherPHID();
$device_phid = $log->getDevicePHID();
if ($device_phid) {
$handle_phids[] = $device_phid;
}
+
+ if ($log->getPushEvent()->getRejectCode() == $reject_herald) {
+ $handle_phids[] = $log->getPushEvent()->getRejectDetails();
+ }
}
- $handles = $viewer->loadHandles($handle_phids);
+ $viewer->loadHandles($handle_phids);
// Only administrators can view remote addresses.
$remotes_visible = $viewer->getIsAdmin();
$flag_map = PhabricatorRepositoryPushLog::getFlagDisplayNames();
$reject_map = PhabricatorRepositoryPushLog::getRejectCodeDisplayNames();
$rows = array();
$any_host = false;
foreach ($logs as $log) {
$repository = $log->getRepository();
if ($remotes_visible) {
$remote_address = $log->getPushEvent()->getRemoteAddress();
} else {
$remote_address = null;
}
$event_id = $log->getPushEvent()->getID();
$old_ref_link = null;
if ($log->getRefOld() != DiffusionCommitHookEngine::EMPTY_HASH) {
$old_ref_link = phutil_tag(
'a',
array(
'href' => $repository->getCommitURI($log->getRefOld()),
),
$log->getRefOldShort());
}
$device_phid = $log->getDevicePHID();
if ($device_phid) {
$device = $viewer->renderHandle($device_phid);
$any_host = true;
} else {
$device = null;
}
$flags = $log->getChangeFlags();
$flag_names = array();
foreach ($flag_map as $flag_key => $flag_name) {
if (($flags & $flag_key) === $flag_key) {
$flag_names[] = $flag_name;
}
}
$flag_names = phutil_implode_html(
phutil_tag('br'),
$flag_names);
$reject_code = $log->getPushEvent()->getRejectCode();
- $reject_label = idx(
- $reject_map,
- $reject_code,
- pht('Unknown ("%s")', $reject_code));
+
+ if ($reject_code == $reject_herald) {
+ $rule_phid = $log->getPushEvent()->getRejectDetails();
+ $handle = $viewer->renderHandle($rule_phid);
+ $reject_label = pht('Blocked: %s', $handle);
+ } else {
+ $reject_label = idx(
+ $reject_map,
+ $reject_code,
+ pht('Unknown ("%s")', $reject_code));
+ }
$rows[] = array(
phutil_tag(
'a',
array(
'href' => '/diffusion/pushlog/view/'.$event_id.'/',
),
$event_id),
phutil_tag(
'a',
array(
'href' => $repository->getURI(),
),
$repository->getDisplayName()),
$viewer->renderHandle($log->getPusherPHID()),
$remote_address,
$log->getPushEvent()->getRemoteProtocol(),
$device,
$log->getRefType(),
$log->getRefName(),
$old_ref_link,
phutil_tag(
'a',
array(
'href' => $repository->getCommitURI($log->getRefNew()),
),
$log->getRefNewShort()),
$flag_names,
$reject_label,
$viewer->formatShortDateTime($log->getEpoch()),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Push'),
pht('Repository'),
pht('Pusher'),
pht('From'),
pht('Via'),
pht('Host'),
pht('Type'),
pht('Name'),
pht('Old'),
pht('New'),
pht('Flags'),
pht('Result'),
pht('Date'),
))
->setColumnClasses(
array(
'',
'',
'',
'',
'',
'',
'',
'wide',
'n',
'n',
'',
'',
'right',
))
->setColumnVisibility(
array(
true,
true,
true,
$remotes_visible,
true,
$any_host,
));
return $table;
}
}
diff --git a/src/applications/diffusion/view/DiffusionReadmeView.php b/src/applications/diffusion/view/DiffusionReadmeView.php
index 693333249..21e97f471 100644
--- a/src/applications/diffusion/view/DiffusionReadmeView.php
+++ b/src/applications/diffusion/view/DiffusionReadmeView.php
@@ -1,113 +1,113 @@
<?php
final class DiffusionReadmeView extends DiffusionView {
private $path;
private $content;
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function getContent() {
return $this->content;
}
/**
* Get the markup language a README should be interpreted as.
*
* @param string Local README path, like "README.txt".
* @return string Best markup interpreter (like "remarkup") for this file.
*/
private function getReadmeLanguage($path) {
$path = phutil_utf8_strtolower($path);
if ($path == 'readme') {
return 'remarkup';
}
$ext = last(explode('.', $path));
switch ($ext) {
case 'remarkup':
case 'md':
return 'remarkup';
case 'rainbow':
return 'rainbow';
case 'txt':
default:
return 'text';
}
}
public function render() {
$readme_path = $this->getPath();
$readme_name = basename($readme_path);
$interpreter = $this->getReadmeLanguage($readme_name);
require_celerity_resource('diffusion-readme-css');
$content = $this->getContent();
$class = null;
switch ($interpreter) {
case 'remarkup':
// TODO: This is sketchy, but make sure we hit the markup cache.
$markup_object = id(new PhabricatorMarkupOneOff())
->setEngineRuleset('diffusion-readme')
->setContent($content);
$markup_field = 'default';
$content = id(new PhabricatorMarkupEngine())
->setViewer($this->getUser())
->addObject($markup_object, $markup_field)
->process()
->getOutput($markup_object, $markup_field);
$engine = $markup_object->newMarkupEngine($markup_field);
$readme_content = $content;
$class = null;
break;
case 'rainbow':
$content = id(new PhutilRainbowSyntaxHighlighter())
->getHighlightFuture($content)
->resolve();
$readme_content = phutil_escape_html_newlines($content);
require_celerity_resource('syntax-highlighting-css');
$class = 'remarkup-code ml';
break;
default:
case 'text':
$readme_content = phutil_escape_html_newlines($content);
$class = 'ml';
break;
}
$readme_content = phutil_tag_div($class, $readme_content);
- $document = id(new PHUIDocumentViewPro())
+ $document = id(new PHUIDocumentView())
->setFluid(true)
->appendChild($readme_content);
$header = id(new PHUIHeaderView())
->setHeader($readme_name)
->addClass('diffusion-panel-header-view');
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->appendChild($document)
->addClass('diffusion-readme-view');
}
}
diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php
index bf69497b1..0c92bd173 100644
--- a/src/applications/diviner/controller/DivinerAtomController.php
+++ b/src/applications/diviner/controller/DivinerAtomController.php
@@ -1,688 +1,688 @@
<?php
final class DivinerAtomController extends DivinerController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$book_name = $request->getURIData('book');
$atom_type = $request->getURIData('type');
$atom_name = $request->getURIData('name');
$atom_context = nonempty($request->getURIData('context'), null);
$atom_index = nonempty($request->getURIData('index'), null);
require_celerity_resource('diviner-shared-css');
$book = id(new DivinerBookQuery())
->setViewer($viewer)
->withNames(array($book_name))
->executeOne();
if (!$book) {
return new Aphront404Response();
}
$symbol = id(new DivinerAtomQuery())
->setViewer($viewer)
->withBookPHIDs(array($book->getPHID()))
->withTypes(array($atom_type))
->withNames(array($atom_name))
->withContexts(array($atom_context))
->withIndexes(array($atom_index))
->withIsDocumentable(true)
->needAtoms(true)
->needExtends(true)
->needChildren(true)
->executeOne();
if (!$symbol) {
return new Aphront404Response();
}
$atom = $symbol->getAtom();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb(
$book->getShortTitle(),
'/book/'.$book->getName().'/');
$atom_short_title = $atom
? $atom->getDocblockMetaValue('short', $symbol->getTitle())
: $symbol->getTitle();
$crumbs->addTextCrumb($atom_short_title);
$header = id(new PHUIHeaderView())
->setHeader($this->renderFullSignature($symbol));
$properties = new PHUIPropertyListView();
$group = $atom ? $atom->getProperty('group') : $symbol->getGroupName();
if ($group) {
$group_name = $book->getGroupName($group);
} else {
$group_name = null;
}
$prop_list = new PHUIPropertyGroupView();
$prop_list->addPropertyList($properties);
- $document = id(new PHUIDocumentViewPro())
+ $document = id(new PHUIDocumentView())
->setBook($book->getTitle(), $group_name)
->setHeader($header)
->addClass('diviner-view');
if ($atom) {
$this->buildDefined($properties, $symbol);
$this->buildExtendsAndImplements($properties, $symbol);
$this->buildRepository($properties, $symbol);
$warnings = $atom->getWarnings();
if ($warnings) {
$warnings = id(new PHUIInfoView())
->setErrors($warnings)
->setTitle(pht('Documentation Warnings'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING);
}
$document->appendChild($warnings);
}
$methods = $this->composeMethods($symbol);
$field = 'default';
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->addObject($symbol, $field);
foreach ($methods as $method) {
foreach ($method['atoms'] as $matom) {
$engine->addObject($matom, $field);
}
}
$engine->process();
if ($atom) {
$content = $this->renderDocumentationText($symbol, $engine);
$document->appendChild($content);
}
$toc = $engine->getEngineMetadata(
$symbol,
$field,
PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC,
array());
if (!$atom) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('This atom no longer exists.')));
}
if ($atom) {
$document->appendChild($this->buildParametersAndReturn(array($symbol)));
}
if ($methods) {
$tasks = $this->composeTasks($symbol);
if ($tasks) {
$methods_by_task = igroup($methods, 'task');
// Add phantom tasks for methods which have a "@task" name that isn't
// documented anywhere, or methods that have no "@task" name.
foreach ($methods_by_task as $task => $ignored) {
if (empty($tasks[$task])) {
$tasks[$task] = array(
'name' => $task,
'title' => $task ? $task : pht('Other Methods'),
'defined' => $symbol,
);
}
}
$section = id(new DivinerSectionView())
->setHeader(pht('Tasks'));
foreach ($tasks as $spec) {
$section->addContent(
id(new PHUIHeaderView())
->setNoBackground(true)
->setHeader($spec['title']));
$task_methods = idx($methods_by_task, $spec['name'], array());
$box_content = array();
if ($task_methods) {
$list_items = array();
foreach ($task_methods as $task_method) {
$atom = last($task_method['atoms']);
$item = $this->renderFullSignature($atom, true);
if (strlen($atom->getSummary())) {
$item = array(
$item,
" \xE2\x80\x94 ",
$atom->getSummary(),
);
}
$list_items[] = phutil_tag('li', array(), $item);
}
$box_content[] = phutil_tag(
'ul',
array(
'class' => 'diviner-list',
),
$list_items);
} else {
$no_methods = pht('No methods for this task.');
$box_content = phutil_tag('em', array(), $no_methods);
}
$inner_box = phutil_tag_div('diviner-task-items', $box_content);
$section->addContent($inner_box);
}
$document->appendChild($section);
}
$section = id(new DivinerSectionView())
->setHeader(pht('Methods'));
foreach ($methods as $spec) {
$matom = last($spec['atoms']);
$method_header = id(new PHUIHeaderView())
->setNoBackground(true);
$inherited = $spec['inherited'];
if ($inherited) {
$method_header->addTag(
id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_GREY)
->setName(pht('Inherited')));
}
$method_header->setHeader($this->renderFullSignature($matom));
$section->addContent(
array(
$method_header,
$this->renderMethodDocumentationText($symbol, $spec, $engine),
$this->buildParametersAndReturn($spec['atoms']),
));
}
$document->appendChild($section);
}
if ($toc) {
$side = new PHUIListView();
$side->addMenuItem(
id(new PHUIListItemView())
->setName(pht('Contents'))
->setType(PHUIListItemView::TYPE_LABEL));
foreach ($toc as $key => $entry) {
$side->addMenuItem(
id(new PHUIListItemView())
->setName($entry[1])
->setHref('#'.$key));
}
$document->setToc($side);
}
$prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list);
return $this->newPage()
->setTitle($symbol->getTitle())
->setCrumbs($crumbs)
->appendChild(array(
$document,
$prop_list,
));
}
private function buildExtendsAndImplements(
PHUIPropertyListView $view,
DivinerLiveSymbol $symbol) {
$lineage = $this->getExtendsLineage($symbol);
if ($lineage) {
$tags = array();
foreach ($lineage as $item) {
$tags[] = $this->renderAtomTag($item);
}
$caret = phutil_tag('span', array('class' => 'caret-right msl msr'));
$tags = phutil_implode_html($caret, $tags);
$view->addProperty(pht('Extends'), $tags);
}
$implements = $this->getImplementsLineage($symbol);
if ($implements) {
$items = array();
foreach ($implements as $spec) {
$via = $spec['via'];
$iface = $spec['interface'];
if ($via == $symbol) {
$items[] = $this->renderAtomTag($iface);
} else {
$items[] = array(
$this->renderAtomTag($iface),
" \xE2\x97\x80 ",
$this->renderAtomTag($via),
);
}
}
$view->addProperty(
pht('Implements'),
phutil_implode_html(phutil_tag('br'), $items));
}
}
private function buildRepository(
PHUIPropertyListView $view,
DivinerLiveSymbol $symbol) {
if (!$symbol->getRepositoryPHID()) {
return;
}
$view->addProperty(
pht('Repository'),
$this->getViewer()->renderHandle($symbol->getRepositoryPHID()));
}
private function renderAtomTag(DivinerLiveSymbol $symbol) {
return id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setName($symbol->getName())
->setHref($symbol->getURI());
}
private function getExtendsLineage(DivinerLiveSymbol $symbol) {
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == 'class') {
$lineage = $this->getExtendsLineage($extends);
$lineage[] = $extends;
return $lineage;
}
}
return array();
}
private function getImplementsLineage(DivinerLiveSymbol $symbol) {
$implements = array();
// Do these first so we get interfaces ordered from most to least specific.
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == 'interface') {
$implements[$extends->getName()] = array(
'interface' => $extends,
'via' => $symbol,
);
}
}
// Now do parent interfaces.
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == 'class') {
$implements += $this->getImplementsLineage($extends);
}
}
return $implements;
}
private function buildDefined(
PHUIPropertyListView $view,
DivinerLiveSymbol $symbol) {
$atom = $symbol->getAtom();
$defined = $atom->getFile().':'.$atom->getLine();
$link = $symbol->getBook()->getConfig('uri.source');
if ($link) {
$link = strtr(
$link,
array(
'%%' => '%',
'%f' => phutil_escape_uri($atom->getFile()),
'%l' => phutil_escape_uri($atom->getLine()),
));
$defined = phutil_tag(
'a',
array(
'href' => $link,
'target' => '_blank',
),
$defined);
}
$view->addProperty(pht('Defined'), $defined);
}
private function composeMethods(DivinerLiveSymbol $symbol) {
$methods = $this->findMethods($symbol);
if (!$methods) {
return $methods;
}
foreach ($methods as $name => $method) {
// Check for "@task" on each parent, to find the most recently declared
// "@task".
$task = null;
foreach ($method['atoms'] as $key => $method_symbol) {
$atom = $method_symbol->getAtom();
if ($atom->getDocblockMetaValue('task')) {
$task = $atom->getDocblockMetaValue('task');
}
}
$methods[$name]['task'] = $task;
// Set 'inherited' if this atom has no implementation of the method.
if (last($method['implementations']) !== $symbol) {
$methods[$name]['inherited'] = true;
} else {
$methods[$name]['inherited'] = false;
}
}
return $methods;
}
private function findMethods(DivinerLiveSymbol $symbol) {
$child_specs = array();
foreach ($symbol->getExtends() as $extends) {
if ($extends->getType() == DivinerAtom::TYPE_CLASS) {
$child_specs = $this->findMethods($extends);
}
}
foreach ($symbol->getChildren() as $child) {
if ($child->getType() == DivinerAtom::TYPE_METHOD) {
$name = $child->getName();
if (isset($child_specs[$name])) {
$child_specs[$name]['atoms'][] = $child;
$child_specs[$name]['implementations'][] = $symbol;
} else {
$child_specs[$name] = array(
'atoms' => array($child),
'defined' => $symbol,
'implementations' => array($symbol),
);
}
}
}
return $child_specs;
}
private function composeTasks(DivinerLiveSymbol $symbol) {
$extends_task_specs = array();
foreach ($symbol->getExtends() as $extends) {
$extends_task_specs += $this->composeTasks($extends);
}
$task_specs = array();
$tasks = $symbol->getAtom()->getDocblockMetaValue('task');
if (strlen($tasks)) {
$tasks = phutil_split_lines($tasks, $retain_endings = false);
foreach ($tasks as $task) {
list($name, $title) = explode(' ', $task, 2);
$name = trim($name);
$title = trim($title);
$task_specs[$name] = array(
'name' => $name,
'title' => $title,
'defined' => $symbol,
);
}
}
$specs = $task_specs + $extends_task_specs;
// Reorder "@tasks" in original declaration order. Basically, we want to
// use the documentation of the closest subclass, but put tasks which
// were declared by parents first.
$keys = array_keys($extends_task_specs);
$specs = array_select_keys($specs, $keys) + $specs;
return $specs;
}
private function renderFullSignature(
DivinerLiveSymbol $symbol,
$is_link = false) {
switch ($symbol->getType()) {
case DivinerAtom::TYPE_CLASS:
case DivinerAtom::TYPE_INTERFACE:
case DivinerAtom::TYPE_METHOD:
case DivinerAtom::TYPE_FUNCTION:
break;
default:
return $symbol->getTitle();
}
$atom = $symbol->getAtom();
$out = array();
if ($atom) {
if ($atom->getProperty('final')) {
$out[] = 'final';
}
if ($atom->getProperty('abstract')) {
$out[] = 'abstract';
}
if ($atom->getProperty('access')) {
$out[] = $atom->getProperty('access');
}
if ($atom->getProperty('static')) {
$out[] = 'static';
}
}
switch ($symbol->getType()) {
case DivinerAtom::TYPE_CLASS:
case DivinerAtom::TYPE_INTERFACE:
$out[] = $symbol->getType();
break;
case DivinerAtom::TYPE_FUNCTION:
switch ($atom->getLanguage()) {
case 'php':
$out[] = $symbol->getType();
break;
}
break;
case DivinerAtom::TYPE_METHOD:
switch ($atom->getLanguage()) {
case 'php':
$out[] = DivinerAtom::TYPE_FUNCTION;
break;
}
break;
}
$anchor = null;
switch ($symbol->getType()) {
case DivinerAtom::TYPE_METHOD:
$anchor = $symbol->getType().'/'.$symbol->getName();
break;
default:
break;
}
$out[] = phutil_tag(
$anchor ? 'a' : 'span',
array(
'class' => 'diviner-atom-signature-name',
'href' => $anchor ? '#'.$anchor : null,
'name' => $is_link ? null : $anchor,
),
$symbol->getName());
$out = phutil_implode_html(' ', $out);
if ($atom) {
$parameters = $atom->getProperty('parameters');
if ($parameters !== null) {
$pout = array();
foreach ($parameters as $parameter) {
$pout[] = idx($parameter, 'name', '...');
}
$out = array($out, '('.implode(', ', $pout).')');
}
}
return phutil_tag(
'span',
array(
'class' => 'diviner-atom-signature',
),
$out);
}
private function buildParametersAndReturn(array $symbols) {
assert_instances_of($symbols, 'DivinerLiveSymbol');
$symbols = array_reverse($symbols);
$out = array();
$collected_parameters = null;
foreach ($symbols as $symbol) {
$parameters = $symbol->getAtom()->getProperty('parameters');
if ($parameters !== null) {
if ($collected_parameters === null) {
$collected_parameters = array();
}
foreach ($parameters as $key => $parameter) {
if (isset($collected_parameters[$key])) {
$collected_parameters[$key] += $parameter;
} else {
$collected_parameters[$key] = $parameter;
}
}
}
}
if (nonempty($parameters)) {
$out[] = id(new DivinerParameterTableView())
->setHeader(pht('Parameters'))
->setParameters($parameters);
}
$collected_return = null;
foreach ($symbols as $symbol) {
$return = $symbol->getAtom()->getProperty('return');
if ($return) {
if ($collected_return) {
$collected_return += $return;
} else {
$collected_return = $return;
}
}
}
if (nonempty($return)) {
$out[] = id(new DivinerReturnTableView())
->setHeader(pht('Return'))
->setReturn($collected_return);
}
return $out;
}
private function renderDocumentationText(
DivinerLiveSymbol $symbol,
PhabricatorMarkupEngine $engine) {
$field = 'default';
$content = $engine->getOutput($symbol, $field);
if (strlen(trim($symbol->getMarkupText($field)))) {
$content = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup diviner-remarkup-section',
),
$content);
} else {
$atom = $symbol->getAtom();
$content = phutil_tag(
'div',
array(
'class' => 'diviner-message-not-documented',
),
DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()));
}
return $content;
}
private function renderMethodDocumentationText(
DivinerLiveSymbol $parent,
array $spec,
PhabricatorMarkupEngine $engine) {
$symbols = array_values($spec['atoms']);
$implementations = array_values($spec['implementations']);
$field = 'default';
$out = array();
foreach ($symbols as $key => $symbol) {
$impl = $implementations[$key];
if ($impl !== $parent) {
if (!strlen(trim($symbol->getMarkupText($field)))) {
continue;
}
}
$doc = $this->renderDocumentationText($symbol, $engine);
if (($impl !== $parent) || $out) {
$where = id(new PHUIBoxView())
->addClass('diviner-method-implementation-header')
->appendChild($impl->getName());
$doc = array($where, $doc);
if ($impl !== $parent) {
$doc = phutil_tag(
'div',
array(
'class' => 'diviner-method-implementation-inherited',
),
$doc);
}
}
$out[] = $doc;
}
// If we only have inherited implementations but none have documentation,
// render the last one here so we get the "this thing has no documentation"
// element.
if (!$out) {
$out[] = $this->renderDocumentationText($symbol, $engine);
}
return $out;
}
}
diff --git a/src/applications/diviner/controller/DivinerBookController.php b/src/applications/diviner/controller/DivinerBookController.php
index 1aa8790ff..4e0ae3524 100644
--- a/src/applications/diviner/controller/DivinerBookController.php
+++ b/src/applications/diviner/controller/DivinerBookController.php
@@ -1,126 +1,126 @@
<?php
final class DivinerBookController extends DivinerController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$book_name = $request->getURIData('book');
$book = id(new DivinerBookQuery())
->setViewer($viewer)
->withNames(array($book_name))
->needRepositories(true)
->executeOne();
if (!$book) {
return new Aphront404Response();
}
$actions = $this->buildActionView($viewer, $book);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb(
$book->getShortTitle(),
'/book/'.$book->getName().'/');
$header = id(new PHUIHeaderView())
->setHeader($book->getTitle())
->setUser($viewer)
->setPolicyObject($book)
->setEpoch($book->getDateModified())
->setActionList($actions);
// TODO: This could probably look better.
if ($book->getRepositoryPHID()) {
$header->addTag(
id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_BLUE)
->setName($book->getRepository()->getMonogram()));
}
- $document = new PHUIDocumentViewPro();
+ $document = new PHUIDocumentView();
$document->setHeader($header);
$document->addClass('diviner-view');
$atoms = id(new DivinerAtomQuery())
->setViewer($viewer)
->withBookPHIDs(array($book->getPHID()))
->withGhosts(false)
->withIsDocumentable(true)
->execute();
$atoms = msort($atoms, 'getSortKey');
$group_spec = $book->getConfig('groups');
if (!is_array($group_spec)) {
$group_spec = array();
}
$groups = mgroup($atoms, 'getGroupName');
$groups = array_select_keys($groups, array_keys($group_spec)) + $groups;
if (isset($groups[''])) {
$no_group = $groups[''];
unset($groups['']);
$groups[''] = $no_group;
}
$out = array();
foreach ($groups as $group => $atoms) {
$group_name = $book->getGroupName($group);
if (!strlen($group_name)) {
$group_name = pht('Free Radicals');
}
$section = id(new DivinerSectionView())
->setHeader($group_name);
$section->addContent($this->renderAtomList($atoms));
$out[] = $section;
}
$preface = $book->getPreface();
$preface_view = null;
if (strlen($preface)) {
$preface_view = new PHUIRemarkupView($viewer, $preface);
}
$document->appendChild($preface_view);
$document->appendChild($out);
return $this->newPage()
->setTitle($book->getTitle())
->setCrumbs($crumbs)
->appendChild(array(
$document,
));
}
private function buildActionView(
PhabricatorUser $user,
DivinerLiveBook $book) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$book,
PhabricatorPolicyCapability::CAN_EDIT);
$action_view = id(new PhabricatorActionListView())
->setUser($user)
->setObject($book);
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Book'))
->setIcon('fa-pencil')
->setHref('/book/'.$book->getName().'/edit/')
->setDisabled(!$can_edit));
return $action_view;
}
}
diff --git a/src/applications/diviner/controller/DivinerMainController.php b/src/applications/diviner/controller/DivinerMainController.php
index e7d179c07..400fb48d0 100644
--- a/src/applications/diviner/controller/DivinerMainController.php
+++ b/src/applications/diviner/controller/DivinerMainController.php
@@ -1,75 +1,75 @@
<?php
final class DivinerMainController extends DivinerController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$books = id(new DivinerBookQuery())
->setViewer($viewer)
->execute();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb(pht('Books'));
$query_button = id(new PHUIButtonView())
->setTag('a')
->setHref($this->getApplicationURI('query/'))
->setText(pht('Advanced Search'))
->setIcon('fa-search');
$header = id(new PHUIHeaderView())
->setHeader(pht('Documentation Books'))
->addActionLink($query_button);
- $document = new PHUIDocumentViewPro();
+ $document = new PHUIDocumentView();
$document->setHeader($header);
$document->addClass('diviner-view');
if ($books) {
$books = msort($books, 'getTitle');
$list = array();
foreach ($books as $book) {
$item = id(new DivinerBookItemView())
->setTitle($book->getTitle())
->setHref('/book/'.$book->getName().'/')
->setSubtitle($book->getPreface());
$list[] = $item;
}
$list = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_MEDIUM_TOP)
->appendChild($list);
$document->appendChild($list);
} else {
$text = pht(
"(NOTE) **Looking for Phabricator documentation?** ".
"If you're looking for help and information about Phabricator, ".
"you can [[https://secure.phabricator.com/diviner/ | ".
"browse the public Phabricator documentation]] on the live site.\n\n".
"Diviner is the documentation generator used to build the ".
"Phabricator documentation.\n\n".
"You haven't generated any Diviner documentation books yet, so ".
"there's nothing to show here. If you'd like to generate your own ".
"local copy of the Phabricator documentation and have it appear ".
"here, run this command:\n\n".
" %s\n\n",
'phabricator/ $ ./bin/diviner generate');
$text = new PHUIRemarkupView($viewer, $text);
$document->appendChild($text);
}
return $this->newPage()
->setTitle(pht('Documentation Books'))
->setCrumbs($crumbs)
->appendChild(array(
$document,
));
}
}
diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php
index 91a911277..18e1d0088 100644
--- a/src/applications/drydock/controller/DrydockLeaseViewController.php
+++ b/src/applications/drydock/controller/DrydockLeaseViewController.php
@@ -1,178 +1,202 @@
<?php
final class DrydockLeaseViewController extends DrydockLeaseController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$lease = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withIDs(array($id))
->needUnconsumedCommands(true)
->executeOne();
if (!$lease) {
return new Aphront404Response();
}
$id = $lease->getID();
$lease_uri = $this->getApplicationURI("lease/{$id}/");
$title = pht('Lease %d', $lease->getID());
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-link')
->setStatus(
$lease->getStatusIcon(),
$lease->getStatusColor(),
$lease->getStatusDisplayName());
if ($lease->isReleasing()) {
$header->addTag(
id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setIcon('fa-exclamation-triangle')
->setColor('red')
->setName('Releasing'));
}
$curtain = $this->buildCurtain($lease);
$properties = $this->buildPropertyListView($lease);
$log_query = id(new DrydockLogQuery())
->withLeasePHIDs(array($lease->getPHID()));
$logs = $this->buildLogBox(
$log_query,
$this->getApplicationURI("lease/{$id}/logs/query/all/"));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title, $lease_uri);
$crumbs->setBorder(true);
$locks = $this->buildLocksTab($lease->getPHID());
$commands = $this->buildCommandsTab($lease->getPHID());
$tab_group = id(new PHUITabGroupView())
->addTab(
id(new PHUITabView())
->setName(pht('Properties'))
->setKey('properties')
->appendChild($properties))
->addTab(
id(new PHUITabView())
->setName(pht('Slot Locks'))
->setKey('locks')
->appendChild($locks))
->addTab(
id(new PHUITabView())
->setName(pht('Commands'))
->setKey('commands')
->appendChild($commands));
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$object_box,
$logs,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function buildCurtain(DrydockLease $lease) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($lease);
$id = $lease->getID();
$can_release = $lease->canRelease();
if ($lease->isReleasing()) {
$can_release = false;
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$lease,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Release Lease'))
->setIcon('fa-times')
->setHref($this->getApplicationURI("/lease/{$id}/release/"))
->setWorkflow(true)
->setDisabled(!$can_release || !$can_edit));
return $curtain;
}
private function buildPropertyListView(
DrydockLease $lease) {
$viewer = $this->getViewer();
$view = new PHUIPropertyListView();
$view->addProperty(
pht('Resource Type'),
$lease->getResourceType());
$owner_phid = $lease->getOwnerPHID();
if ($owner_phid) {
$owner_display = $viewer->renderHandle($owner_phid);
} else {
$owner_display = phutil_tag('em', array(), pht('No Owner'));
}
$view->addProperty(pht('Owner'), $owner_display);
$authorizing_phid = $lease->getAuthorizingPHID();
if ($authorizing_phid) {
$authorizing_display = $viewer->renderHandle($authorizing_phid);
} else {
$authorizing_display = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Authorized By'), $authorizing_display);
$resource_phid = $lease->getResourcePHID();
if ($resource_phid) {
$resource_display = $viewer->renderHandle($resource_phid);
} else {
$resource_display = phutil_tag('em', array(), pht('No Resource'));
}
$view->addProperty(pht('Resource'), $resource_display);
$until = $lease->getUntil();
if ($until) {
$until_display = phabricator_datetime($until, $viewer);
} else {
$until_display = phutil_tag('em', array(), pht('Never'));
}
$view->addProperty(pht('Expires'), $until_display);
+ $acquired_epoch = $lease->getAcquiredEpoch();
+ $activated_epoch = $lease->getActivatedEpoch();
+
+ if ($acquired_epoch) {
+ $acquired_display = phabricator_datetime($acquired_epoch, $viewer);
+ } else {
+ if ($activated_epoch) {
+ $acquired_display = phutil_tag(
+ 'em',
+ array(),
+ pht('Activated on Acquisition'));
+ } else {
+ $acquired_display = phutil_tag('em', array(), pht('Not Acquired'));
+ }
+ }
+ $view->addProperty(pht('Acquired'), $acquired_display);
+
+ if ($activated_epoch) {
+ $activated_display = phabricator_datetime($activated_epoch, $viewer);
+ } else {
+ $activated_display = phutil_tag('em', array(), pht('Not Activated'));
+ }
+ $view->addProperty(pht('Activated'), $activated_display);
+
$attributes = $lease->getAttributes();
if ($attributes) {
$view->addSectionHeader(
pht('Attributes'), 'fa-list-ul');
foreach ($attributes as $key => $value) {
$view->addProperty($key, $value);
}
}
return $view;
}
}
diff --git a/src/applications/drydock/operation/DrydockRepositoryOperationType.php b/src/applications/drydock/operation/DrydockRepositoryOperationType.php
index 7ec3689af..32b562a30 100644
--- a/src/applications/drydock/operation/DrydockRepositoryOperationType.php
+++ b/src/applications/drydock/operation/DrydockRepositoryOperationType.php
@@ -1,43 +1,66 @@
<?php
abstract class DrydockRepositoryOperationType extends Phobject {
private $viewer;
+ private $operation;
+ private $interface;
abstract public function applyOperation(
DrydockRepositoryOperation $operation,
DrydockInterface $interface);
abstract public function getOperationDescription(
DrydockRepositoryOperation $operation,
PhabricatorUser $viewer);
abstract public function getOperationCurrentStatus(
DrydockRepositoryOperation $operation,
PhabricatorUser $viewer);
public function getWorkingCopyMerges(DrydockRepositoryOperation $operation) {
return array();
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
+ final public function setOperation(DrydockRepositoryOperation $operation) {
+ $this->operation = $operation;
+ return $this;
+ }
+
+ final public function getOperation() {
+ return $this->operation;
+ }
+
+ final public function setInterface(DrydockInterface $interface) {
+ $this->interface = $interface;
+ return $this;
+ }
+
+ final public function getInterface() {
+ if (!$this->interface) {
+ throw new PhutilInvalidStateException('setInterface');
+ }
+ return $this->interface;
+ }
+
final public function getOperationConstant() {
return $this->getPhobjectClassConstant('OPCONST', 32);
}
final public static function getAllOperationTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getOperationConstant')
->execute();
}
}
diff --git a/src/applications/drydock/query/DrydockRepositoryOperationQuery.php b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php
index 570e68e82..a5fbe0acc 100644
--- a/src/applications/drydock/query/DrydockRepositoryOperationQuery.php
+++ b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php
@@ -1,171 +1,189 @@
<?php
final class DrydockRepositoryOperationQuery extends DrydockQuery {
private $ids;
private $phids;
private $objectPHIDs;
private $repositoryPHIDs;
private $operationStates;
private $operationTypes;
private $isDismissed;
+ private $authorPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function withOperationStates(array $states) {
$this->operationStates = $states;
return $this;
}
public function withOperationTypes(array $types) {
$this->operationTypes = $types;
return $this;
}
public function withIsDismissed($dismissed) {
$this->isDismissed = $dismissed;
return $this;
}
+ public function withAuthorPHIDs(array $phids) {
+ $this->authorPHIDs = $phids;
+ return $this;
+ }
+
public function newResultObject() {
return new DrydockRepositoryOperation();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $operations) {
$implementations = DrydockRepositoryOperationType::getAllOperationTypes();
+ $viewer = $this->getViewer();
+
foreach ($operations as $key => $operation) {
$impl = idx($implementations, $operation->getOperationType());
if (!$impl) {
$this->didRejectResult($operation);
unset($operations[$key]);
continue;
}
- $impl = clone $impl;
+ $impl = id(clone $impl)
+ ->setViewer($viewer)
+ ->setOperation($operation);
+
$operation->attachImplementation($impl);
}
$repository_phids = mpull($operations, 'getRepositoryPHID');
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
} else {
$repositories = array();
}
foreach ($operations as $key => $operation) {
$repository = idx($repositories, $operation->getRepositoryPHID());
if (!$repository) {
$this->didRejectResult($operation);
unset($operations[$key]);
continue;
}
$operation->attachRepository($repository);
}
return $operations;
}
protected function didFilterPage(array $operations) {
$object_phids = mpull($operations, 'getObjectPHID');
if ($object_phids) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
} else {
$objects = array();
}
foreach ($operations as $key => $operation) {
$object = idx($objects, $operation->getObjectPHID());
$operation->attachObject($object);
}
return $operations;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->operationStates !== null) {
$where[] = qsprintf(
$conn,
'operationState IN (%Ls)',
$this->operationStates);
}
if ($this->operationTypes !== null) {
$where[] = qsprintf(
$conn,
'operationType IN (%Ls)',
$this->operationTypes);
}
if ($this->isDismissed !== null) {
$where[] = qsprintf(
$conn,
'isDismissed = %d',
(int)$this->isDismissed);
}
+ if ($this->authorPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'authorPHID IN (%Ls)',
+ $this->authorPHIDs);
+ }
+
return $where;
}
}
diff --git a/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php b/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php
index 1de32be28..eeb85329b 100644
--- a/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php
+++ b/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php
@@ -1,101 +1,135 @@
<?php
final class DrydockRepositoryOperationSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Drydock Repository Operations');
}
public function getApplicationClassName() {
return 'PhabricatorDrydockApplication';
}
public function newQuery() {
return id(new DrydockRepositoryOperationQuery());
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
+ if ($map['repositoryPHIDs']) {
+ $query->withRepositoryPHIDs($map['repositoryPHIDs']);
+ }
+
+ if ($map['authorPHIDs']) {
+ $query->withAuthorPHIDs($map['authorPHIDs']);
+ }
+
+ if ($map['states']) {
+ $query->withOperationStates($map['states']);
+ }
+
return $query;
}
protected function buildCustomSearchFields() {
return array(
+ id(new PhabricatorSearchDatasourceField())
+ ->setLabel(pht('Repositories'))
+ ->setKey('repositoryPHIDs')
+ ->setAliases(array('repository', 'repositories', 'repositoryPHID'))
+ ->setDatasource(new DiffusionRepositoryFunctionDatasource()),
+
+ // NOTE: Repository operations aren't necessarily created by a real
+ // user, but for now they normally are. Just use a user typeahead until
+ // more use cases arise.
+ id(new PhabricatorUsersSearchField())
+ ->setLabel(pht('Authors'))
+ ->setKey('authorPHIDs')
+ ->setAliases(array('author', 'authors', 'authorPHID')),
+ id(new PhabricatorSearchCheckboxesField())
+ ->setLabel(pht('States'))
+ ->setKey('states')
+ ->setAliases(array('state'))
+ ->setOptions(DrydockRepositoryOperation::getOperationStateNameMap()),
);
}
protected function getURI($path) {
return '/drydock/operation/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Operations'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $operations,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($operations, 'DrydockRepositoryOperation');
$viewer = $this->requireViewer();
$view = new PHUIObjectItemListView();
foreach ($operations as $operation) {
$id = $operation->getID();
$item = id(new PHUIObjectItemView())
->setHeader($operation->getOperationDescription($viewer))
->setHref($this->getApplicationURI("operation/{$id}/"))
->setObjectName(pht('Repository Operation %d', $id));
$state = $operation->getOperationState();
$icon = DrydockRepositoryOperation::getOperationStateIcon($state);
$name = DrydockRepositoryOperation::getOperationStateName($state);
$item->setStatusIcon($icon, $name);
+
+ $created = phabricator_datetime($operation->getDateCreated(), $viewer);
+ $item->addIcon(null, $created);
+
$item->addByline(
array(
pht('Via:'),
' ',
$viewer->renderHandle($operation->getAuthorPHID()),
));
$object_phid = $operation->getObjectPHID();
$repository_phid = $operation->getRepositoryPHID();
$item->addAttribute($viewer->renderHandle($object_phid));
if ($repository_phid !== $object_phid) {
$item->addAttribute($viewer->renderHandle($repository_phid));
}
$view->addItem($item);
}
$result = id(new PhabricatorApplicationSearchResultView())
->setObjectList($view)
->setNoDataString(pht('No matching operations.'));
return $result;
}
}
diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php
index 4cee7a5f1..866bb21b3 100644
--- a/src/applications/drydock/storage/DrydockLease.php
+++ b/src/applications/drydock/storage/DrydockLease.php
@@ -1,518 +1,538 @@
<?php
final class DrydockLease extends DrydockDAO
implements PhabricatorPolicyInterface {
protected $resourcePHID;
protected $resourceType;
protected $until;
protected $ownerPHID;
protected $authorizingPHID;
protected $attributes = array();
protected $status = DrydockLeaseStatus::STATUS_PENDING;
+ protected $acquiredEpoch;
+ protected $activatedEpoch;
private $resource = self::ATTACHABLE;
private $unconsumedCommands = self::ATTACHABLE;
private $releaseOnDestruction;
private $isAcquired = false;
private $isActivated = false;
private $activateWhenAcquired = false;
private $slotLocks = array();
public static function initializeNewLease() {
$lease = new DrydockLease();
// Pregenerate a PHID so that the caller can set something up to release
// this lease before queueing it for activation.
$lease->setPHID($lease->generatePHID());
return $lease;
}
/**
* Flag this lease to be released when its destructor is called. This is
* mostly useful if you have a script which acquires, uses, and then releases
* a lease, as you don't need to explicitly handle exceptions to properly
* release the lease.
*/
public function setReleaseOnDestruction($release) {
$this->releaseOnDestruction = $release;
return $this;
}
public function __destruct() {
if (!$this->releaseOnDestruction) {
return;
}
if (!$this->canRelease()) {
return;
}
$actor = PhabricatorUser::getOmnipotentUser();
$drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
$command = DrydockCommand::initializeNewCommand($actor)
->setTargetPHID($this->getPHID())
->setAuthorPHID($drydock_phid)
->setCommand(DrydockCommand::COMMAND_RELEASE)
->save();
$this->scheduleUpdate();
}
+ public function setStatus($status) {
+ if ($status == DrydockLeaseStatus::STATUS_ACQUIRED) {
+ if (!$this->getAcquiredEpoch()) {
+ $this->setAcquiredEpoch(PhabricatorTime::getNow());
+ }
+ }
+
+ if ($status == DrydockLeaseStatus::STATUS_ACTIVE) {
+ if (!$this->getActivatedEpoch()) {
+ $this->setActivatedEpoch(PhabricatorTime::getNow());
+ }
+ }
+
+ return parent::setStatus($status);
+ }
+
public function getLeaseName() {
return pht('Lease %d', $this->getID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attributes' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'until' => 'epoch?',
'resourceType' => 'text128',
'ownerPHID' => 'phid?',
'resourcePHID' => 'phid?',
+ 'acquiredEpoch' => 'epoch?',
+ 'activatedEpoch' => 'epoch?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_resource' => array(
'columns' => array('resourcePHID', 'status'),
),
'key_status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function setAttribute($key, $value) {
$this->attributes[$key] = $value;
return $this;
}
public function getAttribute($key, $default = null) {
return idx($this->attributes, $key, $default);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST);
}
public function getInterface($type) {
return $this->getResource()->getInterface($this, $type);
}
public function getResource() {
return $this->assertAttached($this->resource);
}
public function attachResource(DrydockResource $resource = null) {
$this->resource = $resource;
return $this;
}
public function hasAttachedResource() {
return ($this->resource !== null);
}
public function getUnconsumedCommands() {
return $this->assertAttached($this->unconsumedCommands);
}
public function attachUnconsumedCommands(array $commands) {
$this->unconsumedCommands = $commands;
return $this;
}
public function isReleasing() {
foreach ($this->getUnconsumedCommands() as $command) {
if ($command->getCommand() == DrydockCommand::COMMAND_RELEASE) {
return true;
}
}
return false;
}
public function queueForActivation() {
if ($this->getID()) {
throw new Exception(
pht('Only new leases may be queued for activation!'));
}
if (!$this->getAuthorizingPHID()) {
throw new Exception(
pht(
'Trying to queue a lease for activation without an authorizing '.
'object. Use "%s" to specify the PHID of the authorizing object. '.
'The authorizing object must be approved to use the allowed '.
'blueprints.',
'setAuthorizingPHID()'));
}
if (!$this->getAllowedBlueprintPHIDs()) {
throw new Exception(
pht(
'Trying to queue a lease for activation without any allowed '.
'Blueprints. Use "%s" to specify allowed blueprints. The '.
'authorizing object must be approved to use the allowed blueprints.',
'setAllowedBlueprintPHIDs()'));
}
$this
->setStatus(DrydockLeaseStatus::STATUS_PENDING)
->save();
$this->scheduleUpdate();
$this->logEvent(DrydockLeaseQueuedLogType::LOGCONST);
return $this;
}
public function setActivateWhenAcquired($activate) {
$this->activateWhenAcquired = true;
return $this;
}
public function needSlotLock($key) {
$this->slotLocks[] = $key;
return $this;
}
public function acquireOnResource(DrydockResource $resource) {
$expect_status = DrydockLeaseStatus::STATUS_PENDING;
$actual_status = $this->getStatus();
if ($actual_status != $expect_status) {
throw new Exception(
pht(
'Trying to acquire a lease on a resource which is in the wrong '.
'state: status must be "%s", actually "%s".',
$expect_status,
$actual_status));
}
if ($this->activateWhenAcquired) {
$new_status = DrydockLeaseStatus::STATUS_ACTIVE;
} else {
$new_status = DrydockLeaseStatus::STATUS_ACQUIRED;
}
if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) {
if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
throw new Exception(
pht(
'Trying to acquire an active lease on a pending resource. '.
'You can not immediately activate leases on resources which '.
'need time to start up.'));
}
}
// Before we associate the lease with the resource, we lock the resource
// and reload it to make sure it is still pending or active. If we don't
// do this, the resource may have just been reclaimed. (Once we acquire
// the resource that stops it from being released, so we're nearly safe.)
$resource_phid = $resource->getPHID();
$hash = PhabricatorHash::digestForIndex($resource_phid);
$lock_key = 'drydock.resource:'.$hash;
$lock = PhabricatorGlobalLock::newLock($lock_key);
try {
$lock->lock(15);
} catch (Exception $ex) {
throw new DrydockResourceLockException(
pht(
'Failed to acquire lock for resource ("%s") while trying to '.
'acquire lease ("%s").',
$resource->getPHID(),
$this->getPHID()));
}
$resource->reload();
if (($resource->getStatus() !== DrydockResourceStatus::STATUS_ACTIVE) &&
($resource->getStatus() !== DrydockResourceStatus::STATUS_PENDING)) {
throw new DrydockAcquiredBrokenResourceException(
pht(
'Trying to acquire lease ("%s") on a resource ("%s") in the '.
'wrong status ("%s").',
$this->getPHID(),
$resource->getPHID(),
$resource->getStatus()));
}
$caught = null;
try {
$this->openTransaction();
try {
DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
$this->slotLocks = array();
} catch (DrydockSlotLockException $ex) {
$this->killTransaction();
$this->logEvent(
DrydockSlotLockFailureLogType::LOGCONST,
array(
'locks' => $ex->getLockMap(),
));
throw $ex;
}
$this
->setResourcePHID($resource->getPHID())
->attachResource($resource)
->setStatus($new_status)
->save();
$this->saveTransaction();
} catch (Exception $ex) {
$caught = $ex;
}
$lock->unlock();
if ($caught) {
throw $caught;
}
$this->isAcquired = true;
$this->logEvent(DrydockLeaseAcquiredLogType::LOGCONST);
if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) {
$this->didActivate();
}
return $this;
}
public function isAcquiredLease() {
return $this->isAcquired;
}
public function activateOnResource(DrydockResource $resource) {
$expect_status = DrydockLeaseStatus::STATUS_ACQUIRED;
$actual_status = $this->getStatus();
if ($actual_status != $expect_status) {
throw new Exception(
pht(
'Trying to activate a lease which has the wrong status: status '.
'must be "%s", actually "%s".',
$expect_status,
$actual_status));
}
if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
// TODO: Be stricter about this?
throw new Exception(
pht(
'Trying to activate a lease on a pending resource.'));
}
$this->openTransaction();
try {
DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
$this->slotLocks = array();
} catch (DrydockSlotLockException $ex) {
$this->killTransaction();
$this->logEvent(
DrydockSlotLockFailureLogType::LOGCONST,
array(
'locks' => $ex->getLockMap(),
));
throw $ex;
}
$this
->setStatus(DrydockLeaseStatus::STATUS_ACTIVE)
->save();
$this->saveTransaction();
$this->isActivated = true;
$this->didActivate();
return $this;
}
public function isActivatedLease() {
return $this->isActivated;
}
public function scheduleUpdate($epoch = null) {
PhabricatorWorker::scheduleTask(
'DrydockLeaseUpdateWorker',
array(
'leasePHID' => $this->getPHID(),
'isExpireTask' => ($epoch !== null),
),
array(
'objectPHID' => $this->getPHID(),
'delayUntil' => ($epoch ? (int)$epoch : null),
));
}
public function setAwakenTaskIDs(array $ids) {
$this->setAttribute('internal.awakenTaskIDs', $ids);
return $this;
}
public function setAllowedBlueprintPHIDs(array $phids) {
$this->setAttribute('internal.blueprintPHIDs', $phids);
return $this;
}
public function getAllowedBlueprintPHIDs() {
return $this->getAttribute('internal.blueprintPHIDs', array());
}
private function didActivate() {
$viewer = PhabricatorUser::getOmnipotentUser();
$need_update = false;
$this->logEvent(DrydockLeaseActivatedLogType::LOGCONST);
$commands = id(new DrydockCommandQuery())
->setViewer($viewer)
->withTargetPHIDs(array($this->getPHID()))
->withConsumed(false)
->execute();
if ($commands) {
$need_update = true;
}
if ($need_update) {
$this->scheduleUpdate();
}
$expires = $this->getUntil();
if ($expires) {
$this->scheduleUpdate($expires);
}
$this->awakenTasks();
}
public function logEvent($type, array $data = array()) {
$log = id(new DrydockLog())
->setEpoch(PhabricatorTime::getNow())
->setType($type)
->setData($data);
$log->setLeasePHID($this->getPHID());
$resource_phid = $this->getResourcePHID();
if ($resource_phid) {
$resource = $this->getResource();
$log->setResourcePHID($resource->getPHID());
$log->setBlueprintPHID($resource->getBlueprintPHID());
}
return $log->save();
}
/**
* Awaken yielded tasks after a state change.
*
* @return this
*/
public function awakenTasks() {
$awaken_ids = $this->getAttribute('internal.awakenTaskIDs');
if (is_array($awaken_ids) && $awaken_ids) {
PhabricatorWorker::awakenTaskIDs($awaken_ids);
}
return $this;
}
public function getURI() {
$id = $this->getID();
return "/drydock/lease/{$id}/";
}
/* -( Status )------------------------------------------------------------- */
public function getStatusObject() {
return DrydockLeaseStatus::newStatusObject($this->getStatus());
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusColor() {
return $this->getStatusObject()->getColor();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function isActivating() {
return $this->getStatusObject()->isActivating();
}
public function isActive() {
return $this->getStatusObject()->isActive();
}
public function canRelease() {
if (!$this->getID()) {
return false;
}
return $this->getStatusObject()->canRelease();
}
public function canReceiveCommands() {
return $this->getStatusObject()->canReceiveCommands();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($this->getResource()) {
return $this->getResource()->getPolicy($capability);
}
// TODO: Implement reasonable policies.
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getResource()) {
return $this->getResource()->hasAutomaticCapability($capability, $viewer);
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('Leases inherit policies from the resources they lease.');
}
}
diff --git a/src/applications/drydock/storage/DrydockRepositoryOperation.php b/src/applications/drydock/storage/DrydockRepositoryOperation.php
index 190b5b131..809a10a11 100644
--- a/src/applications/drydock/storage/DrydockRepositoryOperation.php
+++ b/src/applications/drydock/storage/DrydockRepositoryOperation.php
@@ -1,234 +1,237 @@
<?php
/**
* Represents a request to perform a repository operation like a merge or
* cherry-pick.
*/
final class DrydockRepositoryOperation extends DrydockDAO
implements
PhabricatorPolicyInterface {
const STATE_WAIT = 'wait';
const STATE_WORK = 'work';
const STATE_DONE = 'done';
const STATE_FAIL = 'fail';
protected $authorPHID;
protected $objectPHID;
protected $repositoryPHID;
protected $repositoryTarget;
protected $operationType;
protected $operationState;
protected $properties = array();
protected $isDismissed;
private $repository = self::ATTACHABLE;
private $object = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
public static function initializeNewOperation(
DrydockRepositoryOperationType $op) {
return id(new DrydockRepositoryOperation())
->setOperationState(self::STATE_WAIT)
->setOperationType($op->getOperationConstant())
->setIsDismissed(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'repositoryTarget' => 'bytes',
'operationType' => 'text32',
'operationState' => 'text32',
'isDismissed' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
'key_repository' => array(
'columns' => array('repositoryPHID', 'operationState'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DrydockRepositoryOperationPHIDType::TYPECONST);
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachImplementation(DrydockRepositoryOperationType $impl) {
$this->implementation = $impl;
return $this;
}
public function getImplementation() {
return $this->implementation;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
+ public static function getOperationStateNameMap() {
+ return array(
+ self::STATE_WAIT => pht('Waiting'),
+ self::STATE_WORK => pht('Working'),
+ self::STATE_DONE => pht('Done'),
+ self::STATE_FAIL => pht('Failed'),
+ );
+ }
+
public static function getOperationStateIcon($state) {
$map = array(
self::STATE_WAIT => 'fa-clock-o',
self::STATE_WORK => 'fa-plane ph-spin blue',
self::STATE_DONE => 'fa-check green',
self::STATE_FAIL => 'fa-times red',
);
return idx($map, $state, null);
}
public static function getOperationStateName($state) {
- $map = array(
- self::STATE_WAIT => pht('Waiting'),
- self::STATE_WORK => pht('Working'),
- self::STATE_DONE => pht('Done'),
- self::STATE_FAIL => pht('Failed'),
- );
-
+ $map = self::getOperationStateNameMap();
return idx($map, $state, pht('<Unknown: %s>', $state));
}
public function scheduleUpdate() {
PhabricatorWorker::scheduleTask(
'DrydockRepositoryOperationUpdateWorker',
array(
'operationPHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
}
public function applyOperation(DrydockInterface $interface) {
- return $this->getImplementation()->applyOperation(
- $this,
- $interface);
+ $impl = $this->getImplementation();
+ $impl->setInterface($interface);
+ return $impl->applyOperation($this, $interface);
}
public function getOperationDescription(PhabricatorUser $viewer) {
return $this->getImplementation()->getOperationDescription(
$this,
$viewer);
}
public function getOperationCurrentStatus(PhabricatorUser $viewer) {
return $this->getImplementation()->getOperationCurrentStatus(
$this,
$viewer);
}
public function isUnderway() {
switch ($this->getOperationState()) {
case self::STATE_WAIT:
case self::STATE_WORK:
return true;
}
return false;
}
public function isDone() {
return ($this->getOperationState() === self::STATE_DONE);
}
public function getWorkingCopyMerges() {
return $this->getImplementation()->getWorkingCopyMerges(
$this);
}
public function setWorkingCopyLeasePHID($lease_phid) {
return $this->setProperty('exec.leasePHID', $lease_phid);
}
public function getWorkingCopyLeasePHID() {
return $this->getProperty('exec.leasePHID');
}
public function setCommandError(array $error) {
return $this->setProperty('exec.workingcopy.error', $error);
}
public function getCommandError() {
return $this->getProperty('exec.workingcopy.error');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
$need_capability = $this->getRequiredRepositoryCapability($capability);
return $this->getRepository()
->getPolicy($need_capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$need_capability = $this->getRequiredRepositoryCapability($capability);
return $this->getRepository()
->hasAutomaticCapability($need_capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'A repository operation inherits the policies of the repository it '.
'affects.');
}
private function getRequiredRepositoryCapability($capability) {
// To edit a RepositoryOperation, require that the user be able to push
// to the repository.
$map = array(
PhabricatorPolicyCapability::CAN_EDIT =>
DiffusionPushCapability::CAPABILITY,
);
return idx($map, $capability, $capability);
}
}
diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
index dfd603fd1..2ee9324fb 100644
--- a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
+++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
@@ -1,189 +1,184 @@
<?php
final class DrydockRepositoryOperationUpdateWorker
extends DrydockWorker {
protected function doWork() {
$operation_phid = $this->getTaskDataValue('operationPHID');
$hash = PhabricatorHash::digestForIndex($operation_phid);
$lock_key = 'drydock.operation:'.$hash;
$lock = PhabricatorGlobalLock::newLock($lock_key)
->lock(1);
try {
$operation = $this->loadOperation($operation_phid);
$this->handleUpdate($operation);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
}
private function handleUpdate(DrydockRepositoryOperation $operation) {
- $viewer = $this->getViewer();
-
$operation_state = $operation->getOperationState();
switch ($operation_state) {
case DrydockRepositoryOperation::STATE_WAIT:
$operation
->setOperationState(DrydockRepositoryOperation::STATE_WORK)
->save();
break;
case DrydockRepositoryOperation::STATE_WORK:
break;
case DrydockRepositoryOperation::STATE_DONE:
case DrydockRepositoryOperation::STATE_FAIL:
// No more processing for these requests.
return;
}
// TODO: We should probably check for other running operations with lower
// IDs and the same repository target and yield to them here? That is,
// enforce sequential evaluation of operations against the same target so
// that if you land "A" and then land "B", we always finish "A" first.
// For now, just let stuff happen in any order. We can't lease until
// we know we're good to move forward because we might deadlock if we do:
// we're waiting for another operation to complete, and that operation is
// waiting for a lease we're holding.
try {
- $operation->getImplementation()
- ->setViewer($viewer);
-
$lease = $this->loadWorkingCopyLease($operation);
$interface = $lease->getInterface(
DrydockCommandInterface::INTERFACE_TYPE);
// No matter what happens here, destroy the lease away once we're done.
$lease->setReleaseOnDestruction(true);
$operation->applyOperation($interface);
} catch (PhabricatorWorkerYieldException $ex) {
throw $ex;
} catch (Exception $ex) {
$operation
->setOperationState(DrydockRepositoryOperation::STATE_FAIL)
->save();
throw $ex;
}
$operation
->setOperationState(DrydockRepositoryOperation::STATE_DONE)
->save();
// TODO: Once we have sequencing, we could awaken the next operation
// against this target after finishing or failing.
}
private function loadWorkingCopyLease(
DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
// TODO: This is very similar to leasing in Harbormaster, maybe we can
// share some of the logic?
$working_copy = new DrydockWorkingCopyBlueprintImplementation();
$working_copy_type = $working_copy->getType();
$lease_phid = $operation->getProperty('exec.leasePHID');
if ($lease_phid) {
$lease = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withPHIDs(array($lease_phid))
->executeOne();
if (!$lease) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Lease "%s" could not be loaded.',
$lease_phid));
}
} else {
$repository = $operation->getRepository();
$allowed_phids = $repository->getAutomationBlueprintPHIDs();
$authorizing_phid = $repository->getPHID();
$lease = DrydockLease::initializeNewLease()
->setResourceType($working_copy_type)
->setOwnerPHID($operation->getPHID())
->setAuthorizingPHID($authorizing_phid)
->setAllowedBlueprintPHIDs($allowed_phids);
$map = $this->buildRepositoryMap($operation);
$lease->setAttribute('repositories.map', $map);
$task_id = $this->getCurrentWorkerTaskID();
if ($task_id) {
$lease->setAwakenTaskIDs(array($task_id));
}
$operation
->setWorkingCopyLeasePHID($lease->getPHID())
->save();
$lease->queueForActivation();
}
if ($lease->isActivating()) {
throw new PhabricatorWorkerYieldException(15);
}
if (!$lease->isActive()) {
$vcs_error = $working_copy->getCommandError($lease);
if ($vcs_error) {
$operation
->setCommandError($vcs_error)
->save();
}
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Lease "%s" never activated.',
$lease->getPHID()));
}
return $lease;
}
private function buildRepositoryMap(DrydockRepositoryOperation $operation) {
$repository = $operation->getRepository();
$target = $operation->getRepositoryTarget();
list($type, $name) = explode(':', $target, 2);
switch ($type) {
case 'branch':
$spec = array(
'branch' => $name,
);
break;
case 'none':
$spec = array();
break;
default:
throw new Exception(
pht(
'Unknown repository operation target type "%s" (in target "%s").',
$type,
$target));
}
$spec['merges'] = $operation->getWorkingCopyMerges();
$map = array();
$map[$repository->getCloneName()] = array(
'phid' => $repository->getPHID(),
'default' => true,
) + $spec;
return $map;
}
}
diff --git a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php
index 9afb836b9..7b5d6d072 100644
--- a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php
+++ b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php
@@ -1,84 +1,85 @@
<?php
final class PhabricatorFavoritesMainMenuBarExtension
extends PhabricatorMainMenuBarExtension {
const MAINMENUBARKEY = 'favorites';
public function isExtensionEnabledForViewer(PhabricatorUser $viewer) {
return PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorFavoritesApplication',
$viewer);
}
public function getExtensionOrder() {
return 1100;
}
public function buildMainMenus() {
$viewer = $this->getViewer();
$dropdown = $this->newDropdown($viewer);
if (!$dropdown) {
return null;
}
$favorites_menu = id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setIcon('fa-star')
->addClass('phabricator-core-user-menu')
->setNoCSS(true)
->setDropdown(true)
- ->setDropdownMenu($dropdown);
+ ->setDropdownMenu($dropdown)
+ ->setAuralLabel(pht('Favorites Menu'));
return array(
$favorites_menu,
);
}
private function newDropdown(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array('PhabricatorFavoritesApplication'))
->withInstalled(true)
->execute();
$favorites = head($applications);
if (!$favorites) {
return null;
}
$menu_engine = id(new PhabricatorFavoritesProfileMenuEngine())
->setViewer($viewer)
->setProfileObject($favorites)
->setCustomPHID($viewer->getPHID());
$filter_view = $menu_engine->buildNavigation();
$menu_view = $filter_view->getMenu();
$item_views = $menu_view->getItems();
$view = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($item_views as $item) {
$action = id(new PhabricatorActionView())
->setName($item->getName())
->setHref($item->getHref())
->setType($item->getType());
$view->addAction($action);
}
if ($viewer->isLoggedIn()) {
$view->addAction(
id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Favorites'))
->setHref('/favorites/menu/configure/'));
}
return $view;
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
index bd3d93328..dd5e0adb7 100644
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -1,210 +1,227 @@
<?php
final class PhabricatorFileDataController extends PhabricatorFileController {
private $phid;
private $key;
private $file;
public function shouldRequireLogin() {
return false;
}
public function shouldAllowPartialSessions() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$this->phid = $request->getURIData('phid');
$this->key = $request->getURIData('key');
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$alt_uri = new PhutilURI($alt);
$alt_domain = $alt_uri->getDomain();
$req_domain = $request->getHost();
$main_domain = id(new PhutilURI($base_uri))->getDomain();
$request_kind = $request->getURIData('kind');
$is_download = ($request_kind === 'download');
if (!strlen($alt) || $main_domain == $alt_domain) {
// No alternate domain.
$should_redirect = false;
$is_alternate_domain = false;
} else if ($req_domain != $alt_domain) {
// Alternate domain, but this request is on the main domain.
$should_redirect = true;
$is_alternate_domain = false;
} else {
// Alternate domain, and on the alternate domain.
$should_redirect = false;
$is_alternate_domain = true;
}
$response = $this->loadFile();
if ($response) {
return $response;
}
$file = $this->getFile();
if ($should_redirect) {
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($file->getCDNURI($request_kind));
}
$response = new AphrontFileResponse();
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setCanCDN($file->getCanCDN());
$begin = null;
$end = null;
// NOTE: It's important to accept "Range" requests when playing audio.
// If we don't, Safari has difficulty figuring out how long sounds are
// and glitches when trying to loop them. In particular, Safari sends
// an initial request for bytes 0-1 of the audio file, and things go south
// if we can't respond with a 206 Partial Content.
$range = $request->getHTTPHeader('range');
if (strlen($range)) {
list($begin, $end) = $response->parseHTTPRange($range);
}
if (!$file->isViewableInBrowser()) {
$is_download = true;
}
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
$is_lfs = ($request_type == 'git-lfs');
if (!$is_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
$is_post = $request->isHTTPPost();
+ $is_public = !$viewer->isLoggedIn();
// NOTE: Require POST to download files from the primary domain. If the
// request is not a POST request but arrives on the primary domain, we
// render a confirmation dialog. For discussion, see T13094.
- $is_safe = ($is_alternate_domain || $is_lfs || $is_post);
+ // There are two exceptions to this rule:
+
+ // Git LFS requests can download with GET. This is safe (Git LFS won't
+ // execute files it downloads) and necessary to support Git LFS.
+
+ // Requests with no credentials may also download with GET. This
+ // primarily supports downloading files with `arc download` or other
+ // API clients. This is only "mostly" safe: if you aren't logged in, you
+ // are likely immune to XSS and CSRF. However, an attacker may still be
+ // able to set cookies on this domain (for example, to fixate your
+ // session). For now, we accept these risks because users running
+ // Phabricator in this mode are knowingly accepting a security risk
+ // against setup advice, and there's significant value in having
+ // API development against test and production installs work the same
+ // way.
+
+ $is_safe = ($is_alternate_domain || $is_post || $is_lfs || $is_public);
if (!$is_safe) {
return $this->newDialog()
->setSubmitURI($file->getDownloadURI())
->setTitle(pht('Download File'))
->appendParagraph(
pht(
'Download file %s (%s)?',
phutil_tag('strong', array(), $file->getName()),
phutil_format_bytes($file->getByteSize())))
->addCancelButton($file->getURI())
->addSubmitButton(pht('Download File'));
}
$response->setMimeType($file->getMimeType());
$response->setDownload($file->getName());
}
$iterator = $file->getFileDataIterator($begin, $end);
$response->setContentLength($file->getByteSize());
$response->setContentIterator($iterator);
// In Chrome, we must permit this domain in "object-src" CSP when serving a
// PDF or the browser will refuse to render it.
if (!$is_download && $file->isPDF()) {
$request_uri = id(clone $request->getAbsoluteRequestURI())
->setPath(null)
->setFragment(null)
->setQueryParams(array());
$response->addContentSecurityPolicyURI(
'object-src',
(string)$request_uri);
}
return $response;
}
private function loadFile() {
// Access to files is provided by knowledge of a per-file secret key in
// the URI. Knowledge of this secret is sufficient to retrieve the file.
// For some requests, we also have a valid viewer. However, for many
// requests (like alternate domain requests or Git LFS requests) we will
// not. Even if we do have a valid viewer, use the omnipotent viewer to
// make this logic simpler and more consistent.
// Beyond making the policy check itself more consistent, this also makes
// sure we're consistent about returning HTTP 404 on bad requests instead
// of serving HTTP 200 with a login page, which can mislead some clients.
$viewer = PhabricatorUser::getOmnipotentUser();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($this->phid))
->withIsDeleted(false)
->executeOne();
if (!$file) {
return new Aphront404Response();
}
// We may be on the CDN domain, so we need to use a fully-qualified URI
// here to make sure we end up back on the main domain.
$info_uri = PhabricatorEnv::getURI($file->getInfoURI());
if (!$file->validateSecretKey($this->key)) {
$dialog = $this->newDialog()
->setTitle(pht('Invalid Authorization'))
->appendParagraph(
pht(
'The link you followed to access this file is no longer '.
'valid. The visibility of the file may have changed after '.
'the link was generated.'))
->appendParagraph(
pht(
'You can continue to the file detail page to get more '.
'information and attempt to access the file.'))
->addCancelButton($info_uri, pht('Continue'));
return id(new AphrontDialogResponse())
->setDialog($dialog)
->setHTTPResponseCode(404);
}
if ($file->getIsPartial()) {
$dialog = $this->newDialog()
->setTitle(pht('Partial Upload'))
->appendParagraph(
pht(
'This file has only been partially uploaded. It must be '.
'uploaded completely before you can download it.'))
->appendParagraph(
pht(
'You can continue to the file detail page to monitor the '.
'upload progress of the file.'))
->addCancelButton($info_uri, pht('Continue'));
return id(new AphrontDialogResponse())
->setDialog($dialog)
->setHTTPResponseCode(404);
}
$this->file = $file;
return null;
}
private function getFile() {
if (!$this->file) {
throw new PhutilInvalidStateException('loadFile');
}
return $this->file;
}
}
diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php
index 85219b904..c869f5f0d 100644
--- a/src/applications/files/document/PhabricatorDocumentEngine.php
+++ b/src/applications/files/document/PhabricatorDocumentEngine.php
@@ -1,234 +1,248 @@
<?php
abstract class PhabricatorDocumentEngine
extends Phobject {
private $viewer;
private $highlightedLines = array();
private $encodingConfiguration;
private $highlightingConfiguration;
+ private $blameConfiguration = true;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setHighlightedLines(array $highlighted_lines) {
$this->highlightedLines = $highlighted_lines;
return $this;
}
final public function getHighlightedLines() {
return $this->highlightedLines;
}
final public function canRenderDocument(PhabricatorDocumentRef $ref) {
return $this->canRenderDocumentType($ref);
}
public function canConfigureEncoding(PhabricatorDocumentRef $ref) {
return false;
}
public function canConfigureHighlighting(PhabricatorDocumentRef $ref) {
return false;
}
public function canBlame(PhabricatorDocumentRef $ref) {
return false;
}
final public function setEncodingConfiguration($config) {
$this->encodingConfiguration = $config;
return $this;
}
final public function getEncodingConfiguration() {
return $this->encodingConfiguration;
}
final public function setHighlightingConfiguration($config) {
$this->highlightingConfiguration = $config;
return $this;
}
final public function getHighlightingConfiguration() {
return $this->highlightingConfiguration;
}
+ final public function setBlameConfiguration($blame_configuration) {
+ $this->blameConfiguration = $blame_configuration;
+ return $this;
+ }
+
+ final public function getBlameConfiguration() {
+ return $this->blameConfiguration;
+ }
+
+ final protected function getBlameEnabled() {
+ return $this->blameConfiguration;
+ }
+
public function shouldRenderAsync(PhabricatorDocumentRef $ref) {
return false;
}
abstract protected function canRenderDocumentType(
PhabricatorDocumentRef $ref);
final public function newDocument(PhabricatorDocumentRef $ref) {
$can_complete = $this->canRenderCompleteDocument($ref);
$can_partial = $this->canRenderPartialDocument($ref);
if (!$can_complete && !$can_partial) {
return $this->newMessage(
pht(
'This document is too large to be rendered inline. (The document '.
'is %s bytes, the limit for this engine is %s bytes.)',
new PhutilNumber($ref->getByteLength()),
new PhutilNumber($this->getByteLengthLimit())));
}
return $this->newDocumentContent($ref);
}
final public function newDocumentIcon(PhabricatorDocumentRef $ref) {
return id(new PHUIIconView())
->setIcon($this->getDocumentIconIcon($ref));
}
abstract protected function newDocumentContent(
PhabricatorDocumentRef $ref);
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-file-o';
}
protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) {
return pht('Loading...');
}
final public function getDocumentEngineKey() {
return $this->getPhobjectClassConstant('ENGINEKEY');
}
final public static function getAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getDocumentEngineKey')
->execute();
}
final public function newSortVector(PhabricatorDocumentRef $ref) {
$content_score = $this->getContentScore($ref);
// Prefer engines which can render the entire file over engines which
// can only render a header, and engines which can render a header over
// engines which can't render anything.
if ($this->canRenderCompleteDocument($ref)) {
$limit_score = 0;
} else if ($this->canRenderPartialDocument($ref)) {
$limit_score = 1;
} else {
$limit_score = 2;
}
return id(new PhutilSortVector())
->addInt($limit_score)
->addInt(-$content_score);
}
protected function getContentScore(PhabricatorDocumentRef $ref) {
return 2000;
}
abstract public function getViewAsLabel(PhabricatorDocumentRef $ref);
public function getViewAsIconIcon(PhabricatorDocumentRef $ref) {
$can_complete = $this->canRenderCompleteDocument($ref);
$can_partial = $this->canRenderPartialDocument($ref);
if (!$can_complete && !$can_partial) {
return 'fa-times';
}
return $this->getDocumentIconIcon($ref);
}
public function getViewAsIconColor(PhabricatorDocumentRef $ref) {
$can_complete = $this->canRenderCompleteDocument($ref);
if (!$can_complete) {
return 'grey';
}
return null;
}
final public static function getEnginesForRef(
PhabricatorUser $viewer,
PhabricatorDocumentRef $ref) {
$engines = self::getAllEngines();
foreach ($engines as $key => $engine) {
$engine = id(clone $engine)
->setViewer($viewer);
if (!$engine->canRenderDocument($ref)) {
unset($engines[$key]);
continue;
}
$engines[$key] = $engine;
}
if (!$engines) {
throw new Exception(pht('No content engine can render this document.'));
}
$vectors = array();
foreach ($engines as $key => $usable_engine) {
$vectors[$key] = $usable_engine->newSortVector($ref);
}
$vectors = msortv($vectors, 'getSelf');
return array_select_keys($engines, array_keys($vectors));
}
protected function getByteLengthLimit() {
return (1024 * 1024 * 8);
}
protected function canRenderCompleteDocument(PhabricatorDocumentRef $ref) {
$limit = $this->getByteLengthLimit();
if ($limit) {
$length = $ref->getByteLength();
if ($length > $limit) {
return false;
}
}
return true;
}
protected function canRenderPartialDocument(PhabricatorDocumentRef $ref) {
return false;
}
protected function newMessage($message) {
return phutil_tag(
'div',
array(
'class' => 'document-engine-error',
),
$message);
}
final public function newLoadingContent(PhabricatorDocumentRef $ref) {
$spinner = id(new PHUIIconView())
->setIcon('fa-gear')
->addClass('ph-spin');
return phutil_tag(
'div',
array(
'class' => 'document-engine-loading',
),
array(
$spinner,
$this->getDocumentRenderingText($ref),
));
}
}
diff --git a/src/applications/files/document/PhabricatorDocumentRef.php b/src/applications/files/document/PhabricatorDocumentRef.php
index 953ac7df5..036656c00 100644
--- a/src/applications/files/document/PhabricatorDocumentRef.php
+++ b/src/applications/files/document/PhabricatorDocumentRef.php
@@ -1,154 +1,178 @@
<?php
final class PhabricatorDocumentRef
extends Phobject {
private $name;
private $mimeType;
private $file;
private $byteLength;
private $snippet;
private $symbolMetadata = array();
private $blameURI;
+ private $coverage = array();
public function setFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function getFile() {
return $this->file;
}
public function setMimeType($mime_type) {
$this->mimeType = $mime_type;
return $this;
}
public function getMimeType() {
if ($this->mimeType !== null) {
return $this->mimeType;
}
if ($this->file) {
return $this->file->getMimeType();
}
return null;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
if ($this->name !== null) {
return $this->name;
}
if ($this->file) {
return $this->file->getName();
}
return null;
}
public function setByteLength($length) {
$this->byteLength = $length;
return $this;
}
public function getByteLength() {
if ($this->byteLength !== null) {
return $this->byteLength;
}
if ($this->file) {
return (int)$this->file->getByteSize();
}
return null;
}
public function loadData($begin = null, $end = null) {
if ($this->file) {
$iterator = $this->file->getFileDataIterator($begin, $end);
$result = '';
foreach ($iterator as $chunk) {
$result .= $chunk;
}
return $result;
}
throw new PhutilMethodNotImplementedException();
}
public function hasAnyMimeType(array $candidate_types) {
$mime_full = $this->getMimeType();
$mime_parts = explode(';', $mime_full);
$mime_type = head($mime_parts);
$mime_type = $this->normalizeMimeType($mime_type);
foreach ($candidate_types as $candidate_type) {
if ($this->normalizeMimeType($candidate_type) === $mime_type) {
return true;
}
}
return false;
}
private function normalizeMimeType($mime_type) {
$mime_type = trim($mime_type);
$mime_type = phutil_utf8_strtolower($mime_type);
return $mime_type;
}
public function isProbablyText() {
$snippet = $this->getSnippet();
return (strpos($snippet, "\0") === false);
}
public function isProbablyJSON() {
if (!$this->isProbablyText()) {
return false;
}
$snippet = $this->getSnippet();
- if (!preg_match('/^\s*[{[]/', $snippet)) {
+
+ // If the file is longer than the snippet, we don't detect the content
+ // as JSON. We could use some kind of heuristic here if we wanted, but
+ // see PHI749 for a false positive.
+ if (strlen($snippet) < $this->getByteLength()) {
return false;
}
- return phutil_is_utf8($snippet);
+ // If the snippet is the whole file, just check if the snippet is valid
+ // JSON. Note that `phutil_json_decode()` only accepts arrays and objects
+ // as JSON, so this won't misfire on files with content like "3".
+ try {
+ phutil_json_decode($snippet);
+ return true;
+ } catch (Exception $ex) {
+ return false;
+ }
}
public function getSnippet() {
if ($this->snippet === null) {
$this->snippet = $this->loadData(null, (1024 * 1024 * 1));
}
return $this->snippet;
}
public function setSymbolMetadata(array $metadata) {
$this->symbolMetadata = $metadata;
return $this;
}
public function getSymbolMetadata() {
return $this->symbolMetadata;
}
public function setBlameURI($blame_uri) {
$this->blameURI = $blame_uri;
return $this;
}
public function getBlameURI() {
return $this->blameURI;
}
+ public function addCoverage($coverage) {
+ $this->coverage[] = array(
+ 'data' => $coverage,
+ );
+ return $this;
+ }
+
+ public function getCoverage() {
+ return $this->coverage;
+ }
+
}
diff --git a/src/applications/files/document/PhabricatorSourceDocumentEngine.php b/src/applications/files/document/PhabricatorSourceDocumentEngine.php
index 20a1c94a9..5d7c0cdb7 100644
--- a/src/applications/files/document/PhabricatorSourceDocumentEngine.php
+++ b/src/applications/files/document/PhabricatorSourceDocumentEngine.php
@@ -1,66 +1,70 @@
<?php
final class PhabricatorSourceDocumentEngine
extends PhabricatorTextDocumentEngine {
const ENGINEKEY = 'source';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return pht('View as Source');
}
public function canConfigureHighlighting(PhabricatorDocumentRef $ref) {
return true;
}
public function canBlame(PhabricatorDocumentRef $ref) {
return true;
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-code';
}
protected function getContentScore(PhabricatorDocumentRef $ref) {
return 1500;
}
protected function newDocumentContent(PhabricatorDocumentRef $ref) {
$content = $this->loadTextData($ref);
$messages = array();
$highlighting = $this->getHighlightingConfiguration();
if ($highlighting !== null) {
$content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
$highlighting,
$content);
} else {
$highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT;
if (strlen($content) > $highlight_limit) {
$messages[] = $this->newMessage(
pht(
'This file is larger than %s, so syntax highlighting was skipped.',
phutil_format_bytes($highlight_limit)));
} else {
$content = PhabricatorSyntaxHighlighter::highlightWithFilename(
$ref->getName(),
$content);
}
}
$options = array();
- if ($ref->getBlameURI()) {
+ if ($ref->getBlameURI() && $this->getBlameEnabled()) {
$content = phutil_split_lines($content);
$blame = range(1, count($content));
$blame = array_fuse($blame);
$options['blame'] = $blame;
}
+ if ($ref->getCoverage()) {
+ $options['coverage'] = $ref->getCoverage();
+ }
+
return array(
$messages,
$this->newTextDocumentContent($ref, $content, $options),
);
}
}
diff --git a/src/applications/files/document/PhabricatorTextDocumentEngine.php b/src/applications/files/document/PhabricatorTextDocumentEngine.php
index 0377a2435..2fce98baf 100644
--- a/src/applications/files/document/PhabricatorTextDocumentEngine.php
+++ b/src/applications/files/document/PhabricatorTextDocumentEngine.php
@@ -1,109 +1,115 @@
<?php
abstract class PhabricatorTextDocumentEngine
extends PhabricatorDocumentEngine {
private $encodingMessage = null;
protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
return $ref->isProbablyText();
}
public function canConfigureEncoding(PhabricatorDocumentRef $ref) {
return true;
}
protected function newTextDocumentContent(
PhabricatorDocumentRef $ref,
$content,
array $options = array()) {
PhutilTypeSpec::checkMap(
$options,
array(
'blame' => 'optional wild',
+ 'coverage' => 'optional list<wild>',
));
if (is_array($content)) {
$lines = $content;
} else {
$lines = phutil_split_lines($content);
}
$view = id(new PhabricatorSourceCodeView())
->setHighlights($this->getHighlightedLines())
->setLines($lines)
->setSymbolMetadata($ref->getSymbolMetadata());
$blame = idx($options, 'blame');
if ($blame !== null) {
$view->setBlameMap($blame);
}
+ $coverage = idx($options, 'coverage');
+ if ($coverage !== null) {
+ $view->setCoverage($coverage);
+ }
+
$message = null;
if ($this->encodingMessage !== null) {
$message = $this->newMessage($this->encodingMessage);
}
$container = phutil_tag(
'div',
array(
'class' => 'document-engine-text',
),
array(
$message,
$view,
));
return $container;
}
protected function loadTextData(PhabricatorDocumentRef $ref) {
$content = $ref->loadData();
$encoding = $this->getEncodingConfiguration();
if ($encoding !== null) {
if (function_exists('mb_convert_encoding')) {
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
$this->encodingMessage = pht(
'This document was converted from %s to UTF8 for display.',
$encoding);
} else {
$this->encodingMessage = pht(
'Unable to perform text encoding conversion: mbstring extension '.
'is not available.');
}
} else {
if (!phutil_is_utf8($content)) {
if (function_exists('mb_detect_encoding')) {
$try_encodings = array(
'JIS' => pht('JIS'),
'EUC-JP' => pht('EUC-JP'),
'SJIS' => pht('Shift JIS'),
'ISO-8859-1' => pht('ISO-8859-1 (Latin 1)'),
);
$guess = mb_detect_encoding($content, array_keys($try_encodings));
if ($guess) {
$content = mb_convert_encoding($content, 'UTF-8', $guess);
$this->encodingMessage = pht(
'This document is not UTF8. It was detected as %s and '.
'converted to UTF8 for display.',
idx($try_encodings, $guess, $guess));
}
}
}
}
if (!phutil_is_utf8($content)) {
$content = phutil_utf8ize($content);
$this->encodingMessage = pht(
'This document is not UTF8 and its text encoding could not be '.
'detected automatically. Use "Change Text Encoding..." to choose '.
'an encoding.');
}
return $content;
}
}
diff --git a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php
index 4a16f53fd..2524f7782 100644
--- a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php
+++ b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php
@@ -1,310 +1,338 @@
<?php
abstract class PhabricatorDocumentRenderingEngine
extends Phobject {
private $request;
private $controller;
private $activeEngine;
private $ref;
final public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
final public function getRequest() {
if (!$this->request) {
throw new PhutilInvalidStateException('setRequest');
}
return $this->request;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
final public function getController() {
if (!$this->controller) {
throw new PhutilInvalidStateException('setController');
}
return $this->controller;
}
final protected function getActiveEngine() {
return $this->activeEngine;
}
final protected function getRef() {
return $this->ref;
}
final public function newDocumentView(PhabricatorDocumentRef $ref) {
$request = $this->getRequest();
$viewer = $request->getViewer();
$engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
$engine_key = $this->getSelectedDocumentEngineKey();
if (!isset($engines[$engine_key])) {
$engine_key = head_key($engines);
}
$engine = $engines[$engine_key];
$lines = $this->getSelectedLineRange();
if ($lines) {
$engine->setHighlightedLines(range($lines[0], $lines[1]));
}
$encode_setting = $request->getStr('encode');
if (strlen($encode_setting)) {
$engine->setEncodingConfiguration($encode_setting);
}
$highlight_setting = $request->getStr('highlight');
if (strlen($highlight_setting)) {
$engine->setHighlightingConfiguration($highlight_setting);
}
+ $blame_setting = ($request->getStr('blame') !== 'off');
+ $engine->setBlameConfiguration($blame_setting);
+
$views = array();
foreach ($engines as $candidate_key => $candidate_engine) {
$label = $candidate_engine->getViewAsLabel($ref);
if ($label === null) {
continue;
}
$view_uri = $this->newRefViewURI($ref, $candidate_engine);
$view_icon = $candidate_engine->getViewAsIconIcon($ref);
$view_color = $candidate_engine->getViewAsIconColor($ref);
$loading = $candidate_engine->newLoadingContent($ref);
$views[] = array(
'viewKey' => $candidate_engine->getDocumentEngineKey(),
'icon' => $view_icon,
'color' => $view_color,
'name' => $label,
'engineURI' => $this->newRefRenderURI($ref, $candidate_engine),
'viewURI' => $view_uri,
'loadingMarkup' => hsprintf('%s', $loading),
'canEncode' => $candidate_engine->canConfigureEncoding($ref),
'canHighlight' => $candidate_engine->canConfigureHighlighting($ref),
'canBlame' => $candidate_engine->canBlame($ref),
);
}
$viewport_id = celerity_generate_unique_node_id();
$control_id = celerity_generate_unique_node_id();
$icon = $engine->newDocumentIcon($ref);
$config = array(
'controlID' => $control_id,
);
+ $this->willStageRef($ref);
+
if ($engine->shouldRenderAsync($ref)) {
$content = $engine->newLoadingContent($ref);
$config['next'] = 'render';
} else {
$this->willRenderRef($ref);
$content = $engine->newDocument($ref);
if ($engine->canBlame($ref)) {
$config['next'] = 'blame';
}
}
Javelin::initBehavior('document-engine', $config);
$viewport = phutil_tag(
'div',
array(
'id' => $viewport_id,
),
$content);
$meta = array(
'viewportID' => $viewport_id,
'viewKey' => $engine->getDocumentEngineKey(),
'views' => $views,
'encode' => array(
'icon' => 'fa-font',
'name' => pht('Change Text Encoding...'),
'uri' => '/services/encoding/',
'value' => $encode_setting,
),
'highlight' => array(
'icon' => 'fa-lightbulb-o',
'name' => pht('Highlight As...'),
'uri' => '/services/highlight/',
'value' => $highlight_setting,
),
'blame' => array(
+ 'icon' => 'fa-backward',
+ 'hide' => pht('Hide Blame'),
+ 'show' => pht('Show Blame'),
'uri' => $ref->getBlameURI(),
+ 'enabled' => $blame_setting,
'value' => null,
),
+ 'coverage' => array(
+ 'labels' => array(
+ // TODO: Modularize this properly, see T13125.
+ array(
+ 'C' => pht('Covered'),
+ 'U' => pht('Not Covered'),
+ 'N' => pht('Not Executable'),
+ 'X' => pht('Not Reachable'),
+ ),
+ ),
+ ),
);
$view_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View Options'))
->setIcon('fa-file-image-o')
->setColor(PHUIButtonView::GREY)
->setID($control_id)
->setMetadata($meta)
->setDropdown(true)
->addSigil('document-engine-view-dropdown');
$header = id(new PHUIHeaderView())
->setHeaderIcon($icon)
->setHeader($ref->getName())
->addActionLink($view_button);
return id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeader($header)
->appendChild($viewport);
}
final public function newRenderResponse(PhabricatorDocumentRef $ref) {
+ $this->willStageRef($ref);
$this->willRenderRef($ref);
$request = $this->getRequest();
$viewer = $request->getViewer();
$engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
$engine_key = $this->getSelectedDocumentEngineKey();
if (!isset($engines[$engine_key])) {
return $this->newErrorResponse(
pht(
'The engine ("%s") is unknown, or unable to render this document.',
$engine_key));
}
$engine = $engines[$engine_key];
$this->activeEngine = $engine;
$encode_setting = $request->getStr('encode');
if (strlen($encode_setting)) {
$engine->setEncodingConfiguration($encode_setting);
}
$highlight_setting = $request->getStr('highlight');
if (strlen($highlight_setting)) {
$engine->setHighlightingConfiguration($highlight_setting);
}
+ $blame_setting = ($request->getStr('blame') !== 'off');
+ $engine->setBlameConfiguration($blame_setting);
+
try {
$content = $engine->newDocument($ref);
} catch (Exception $ex) {
return $this->newErrorResponse($ex->getMessage());
}
return $this->newContentResponse($content);
}
public function newErrorResponse($message) {
$container = phutil_tag(
'div',
array(
'class' => 'document-engine-error',
),
array(
id(new PHUIIconView())
->setIcon('fa-exclamation-triangle red'),
' ',
$message,
));
return $this->newContentResponse($container);
}
private function newContentResponse($content) {
$request = $this->getRequest();
$viewer = $request->getViewer();
$controller = $this->getController();
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'markup' => hsprintf('%s', $content),
));
}
$crumbs = $this->newCrumbs();
$crumbs->setBorder(true);
$content_frame = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($content);
$page_frame = id(new PHUITwoColumnView())
->setFooter($content_frame);
$title = array();
$ref = $this->getRef();
if ($ref) {
$title = array(
$ref->getName(),
pht('Standalone'),
);
} else {
$title = pht('Document');
}
return $controller->newPage()
->setCrumbs($crumbs)
->setTitle($title)
->appendChild($page_frame);
}
protected function newCrumbs() {
$engine = $this->getActiveEngine();
$controller = $this->getController();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
$ref = $this->getRef();
$this->addApplicationCrumbs($crumbs, $ref);
if ($ref) {
$label = $engine->getViewAsLabel($ref);
if ($label) {
$crumbs->addTextCrumb($label);
}
}
return $crumbs;
}
abstract protected function newRefViewURI(
PhabricatorDocumentRef $ref,
PhabricatorDocumentEngine $engine);
abstract protected function newRefRenderURI(
PhabricatorDocumentRef $ref,
PhabricatorDocumentEngine $engine);
protected function getSelectedDocumentEngineKey() {
return $this->getRequest()->getURIData('engineKey');
}
protected function getSelectedLineRange() {
return $this->getRequest()->getURILineRange('lines', 1000);
}
protected function addApplicationCrumbs(
PHUICrumbsView $crumbs,
PhabricatorDocumentRef $ref = null) {
return;
}
+ protected function willStageRef(PhabricatorDocumentRef $ref) {
+ return;
+ }
+
protected function willRenderRef(PhabricatorDocumentRef $ref) {
return;
}
}
diff --git a/src/applications/files/favicon/PhabricatorFaviconRef.php b/src/applications/files/favicon/PhabricatorFaviconRef.php
index d6c2fd80a..08ade5283 100644
--- a/src/applications/files/favicon/PhabricatorFaviconRef.php
+++ b/src/applications/files/favicon/PhabricatorFaviconRef.php
@@ -1,447 +1,454 @@
<?php
final class PhabricatorFaviconRef extends Phobject {
private $viewer;
private $width;
private $height;
private $emblems;
private $uri;
private $cacheKey;
public function __construct() {
$this->emblems = array(null, null, null, null);
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setWidth($width) {
$this->width = $width;
return $this;
}
public function getWidth() {
return $this->width;
}
public function setHeight($height) {
$this->height = $height;
return $this;
}
public function getHeight() {
return $this->height;
}
public function setEmblems(array $emblems) {
if (count($emblems) !== 4) {
throw new Exception(
pht(
'Expected four elements in icon emblem list. To omit an emblem, '.
'pass "null".'));
}
$this->emblems = $emblems;
return $this;
}
public function getEmblems() {
return $this->emblems;
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setCacheKey($cache_key) {
$this->cacheKey = $cache_key;
return $this;
}
public function getCacheKey() {
return $this->cacheKey;
}
public function newDigest() {
return PhabricatorHash::digestForIndex(serialize($this->toDictionary()));
}
public function toDictionary() {
return array(
'width' => $this->width,
'height' => $this->height,
'emblems' => $this->emblems,
);
}
public static function newConfigurationDigest() {
$all_resources = self::getAllResources();
// Because we need to access this cache on every page, it's very sticky.
// Try to dirty it automatically if any relevant configuration changes.
$inputs = array(
'resources' => $all_resources,
'prod' => PhabricatorEnv::getProductionURI('/'),
'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'),
'havepng' => function_exists('imagepng'),
);
return PhabricatorHash::digestForIndex(serialize($inputs));
}
private static function getAllResources() {
$custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons');
foreach ($custom_resources as $key => $custom_resource) {
$custom_resources[$key] = array(
'source-type' => 'file',
'default' => false,
) + $custom_resource;
}
$builtin_resources = self::getBuiltinResources();
return array_merge($builtin_resources, $custom_resources);
}
private static function getBuiltinResources() {
return array(
array(
'source-type' => 'builtin',
'source' => 'favicon/default-76x76.png',
'version' => 1,
'width' => 76,
'height' => 76,
'default' => true,
),
array(
'source-type' => 'builtin',
'source' => 'favicon/default-120x120.png',
'version' => 1,
'width' => 120,
'height' => 120,
'default' => true,
),
array(
'source-type' => 'builtin',
'source' => 'favicon/default-128x128.png',
'version' => 1,
'width' => 128,
'height' => 128,
'default' => true,
),
array(
'source-type' => 'builtin',
'source' => 'favicon/default-152x152.png',
'version' => 1,
'width' => 152,
'height' => 152,
'default' => true,
),
array(
'source-type' => 'builtin',
'source' => 'favicon/dot-pink-64x64.png',
'version' => 1,
'width' => 64,
'height' => 64,
'emblem' => 'dot-pink',
'default' => true,
),
array(
'source-type' => 'builtin',
'source' => 'favicon/dot-red-64x64.png',
'version' => 1,
'width' => 64,
'height' => 64,
'emblem' => 'dot-red',
'default' => true,
),
);
}
public function newURI() {
$dst_w = $this->getWidth();
$dst_h = $this->getHeight();
$template = $this->newTemplateFile(null, $dst_w, $dst_h);
$template_file = $template['file'];
$cache = $this->loadCachedFile($template_file);
if ($cache) {
return $cache->getViewURI();
}
$data = $this->newCompositedFavicon($template);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$caught = null;
try {
$favicon_file = $this->newFaviconFile($data);
$xform = id(new PhabricatorTransformedFile())
->setOriginalPHID($template_file->getPHID())
->setTransformedPHID($favicon_file->getPHID())
->setTransform($this->getCacheKey());
try {
$xform->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
unset($unguarded);
$cache = $this->loadCachedFile($template_file);
if (!$cache) {
throw $ex;
}
id(new PhabricatorDestructionEngine())
->destroyObject($favicon_file);
return $cache->getViewURI();
}
} catch (Exception $ex) {
$caught = $ex;
}
unset($unguarded);
if ($caught) {
throw $caught;
}
return $favicon_file->getViewURI();
}
private function loadCachedFile(PhabricatorFile $template_file) {
$viewer = $this->getViewer();
$xform = id(new PhabricatorTransformedFile())->loadOneWhere(
'originalPHID = %s AND transform = %s',
$template_file->getPHID(),
$this->getCacheKey());
if (!$xform) {
return null;
}
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($xform->getTransformedPHID()))
->executeOne();
}
private function newCompositedFavicon($template) {
$dst_w = $this->getWidth();
$dst_h = $this->getHeight();
$src_w = $template['width'];
$src_h = $template['height'];
- $template_data = $template['file']->loadFileData();
+ try {
+ $template_data = $template['file']->loadFileData();
+ } catch (Exception $ex) {
+ // In rare cases, we can end up with a corrupted or inaccessible file.
+ // If we do, just give up: otherwise, it's impossible to get pages to
+ // generate and not obvious how to fix it.
+ return null;
+ }
if (!function_exists('imagecreatefromstring')) {
return $template_data;
}
$src = @imagecreatefromstring($template_data);
if (!$src) {
return $template_data;
}
$dst = imagecreatetruecolor($dst_w, $dst_h);
imagesavealpha($dst, true);
$transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127);
imagefill($dst, 0, 0, $transparent);
imagecopyresampled(
$dst,
$src,
0,
0,
0,
0,
$dst_w,
$dst_h,
$src_w,
$src_h);
// Now, copy any icon emblems on top of the image. These are dots or other
// marks used to indicate status information.
$emblem_w = (int)floor(min($dst_w, $dst_h) / 2);
$emblem_h = $emblem_w;
foreach ($this->emblems as $key => $emblem) {
if ($emblem === null) {
continue;
}
$emblem_template = $this->newTemplateFile(
$emblem,
$emblem_w,
$emblem_h);
switch ($key) {
case 0:
$emblem_x = $dst_w - $emblem_w;
$emblem_y = 0;
break;
case 1:
$emblem_x = $dst_w - $emblem_w;
$emblem_y = $dst_h - $emblem_h;
break;
case 2:
$emblem_x = 0;
$emblem_y = $dst_h - $emblem_h;
break;
case 3:
$emblem_x = 0;
$emblem_y = 0;
break;
}
$emblem_data = $emblem_template['file']->loadFileData();
$src = @imagecreatefromstring($emblem_data);
if (!$src) {
continue;
}
imagecopyresampled(
$dst,
$src,
$emblem_x,
$emblem_y,
0,
0,
$emblem_w,
$emblem_h,
$emblem_template['width'],
$emblem_template['height']);
}
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
$dst,
'image/png');
}
private function newTemplateFile($emblem, $width, $height) {
$all_resources = self::getAllResources();
$scores = array();
$ratio = $width / $height;
foreach ($all_resources as $key => $resource) {
// We can't use an emblem resource for a different emblem, nor for an
// icon base. We also can't use an icon base as an emblem. That is, if
// we're looking for a picture of a red dot, we have to actually find
// a red dot, not just any image which happens to have a similar size.
if (idx($resource, 'emblem') !== $emblem) {
continue;
}
$resource_width = $resource['width'];
$resource_height = $resource['height'];
// Never use a resource with a different aspect ratio.
if (($resource_width / $resource_height) !== $ratio) {
continue;
}
// Try to use custom resources instead of default resources.
if ($resource['default']) {
$default_score = 1;
} else {
$default_score = 0;
}
$width_diff = ($resource_width - $width);
// If we have to resize an image, we'd rather scale a larger image down
// than scale a smaller image up.
if ($width_diff < 0) {
$scale_score = 1;
} else {
$scale_score = 0;
}
// Otherwise, we'd rather scale an image a little bit (ideally, zero)
// than scale an image a lot.
$width_score = abs($width_diff);
$scores[$key] = id(new PhutilSortVector())
->addInt($default_score)
->addInt($scale_score)
->addInt($width_score);
}
if (!$scores) {
if ($emblem === null) {
throw new Exception(
pht(
'Found no background template resource for dimensions %dx%d.',
$width,
$height));
} else {
throw new Exception(
pht(
'Found no template resource (for emblem "%s") with dimensions '.
'%dx%d.',
$emblem,
$width,
$height));
}
}
$scores = msortv($scores, 'getSelf');
$best_score = head_key($scores);
$viewer = $this->getViewer();
$resource = $all_resources[$best_score];
if ($resource['source-type'] === 'builtin') {
$file = PhabricatorFile::loadBuiltin($viewer, $resource['source']);
if (!$file) {
throw new Exception(
pht(
'Failed to load favicon template builtin "%s".',
$resource['source']));
}
} else {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($resource['source']))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load favicon template with PHID "%s".',
$resource['source']));
}
}
return array(
'width' => $resource['width'],
'height' => $resource['height'],
'file' => $file,
);
}
private function newFaviconFile($data) {
return PhabricatorFile::newFromFileData(
$data,
array(
'name' => 'favicon',
'canCDN' => true,
));
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
index eda43e2ef..909e45c31 100644
--- a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
@@ -1,221 +1,247 @@
<?php
final class PhabricatorFilesManagementMigrateWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('migrate')
->setSynopsis(pht('Migrate files between storage engines.'))
->setArguments(
array(
array(
'name' => 'engine',
'param' => 'storage_engine',
'help' => pht('Migrate to the named storage engine.'),
),
array(
'name' => 'dry-run',
'help' => pht('Show what would be migrated.'),
),
array(
'name' => 'min-size',
'param' => 'bytes',
'help' => pht(
'Do not migrate data for files which are smaller than a given '.
'filesize.'),
),
array(
'name' => 'max-size',
'param' => 'bytes',
'help' => pht(
'Do not migrate data for files which are larger than a given '.
'filesize.'),
),
array(
'name' => 'all',
'help' => pht('Migrate all files.'),
),
array(
'name' => 'copy',
'help' => pht(
'Copy file data instead of moving it: after migrating, do not '.
'remove the old data even if it is no longer referenced.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$target_key = $args->getArg('engine');
if (!$target_key) {
throw new PhutilArgumentUsageException(
pht(
'Specify an engine to migrate to with `%s`. '.
'Use `%s` to get a list of engines.',
'--engine',
'files engines'));
}
$target_engine = PhabricatorFile::buildEngine($target_key);
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
pht(
'Either specify a list of files to migrate, or use `%s` '.
'to migrate all files.',
'--all'));
}
$is_dry_run = $args->getArg('dry-run');
$min_size = (int)$args->getArg('min-size');
$max_size = (int)$args->getArg('max-size');
$is_copy = $args->getArg('copy');
$failed = array();
$engines = PhabricatorFileStorageEngine::loadAllEngines();
$total_bytes = 0;
$total_files = 0;
foreach ($iterator as $file) {
$monogram = $file->getMonogram();
+ // See T7148. When we export data for an instance, we copy all the data
+ // for Files from S3 into the database dump so that the database dump is
+ // a complete, standalone archive of all the data. In the general case,
+ // installs may have a similar process using "--copy" to create a more
+ // complete backup.
+
+ // When doing this, we may run into temporary files which have been
+ // deleted between the time we took the original dump and the current
+ // timestamp. These files can't be copied since the data no longer
+ // exists: the daemons on the live install already deleted it.
+
+ // Simply avoid this whole mess by declining to migrate expired temporary
+ // files. They're as good as dead anyway.
+
+ $ttl = $file->getTTL();
+ if ($ttl) {
+ if ($ttl < PhabricatorTime::getNow()) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ '%s: Skipping expired temporary file.',
+ $monogram));
+ continue;
+ }
+ }
+
$engine_key = $file->getStorageEngine();
$engine = idx($engines, $engine_key);
if (!$engine) {
echo tsprintf(
"%s\n",
pht(
'%s: Uses unknown storage engine "%s".',
$monogram,
$engine_key));
$failed[] = $file;
continue;
}
if ($engine->isChunkEngine()) {
echo tsprintf(
"%s\n",
pht(
'%s: Stored as chunks, no data to migrate directly.',
$monogram));
continue;
}
if ($engine_key === $target_key) {
echo tsprintf(
"%s\n",
pht(
'%s: Already stored in engine "%s".',
$monogram,
$target_key));
continue;
}
$byte_size = $file->getByteSize();
if ($min_size && ($byte_size < $min_size)) {
echo tsprintf(
"%s\n",
pht(
'%s: File size (%s) is smaller than minimum size (%s).',
$monogram,
phutil_format_bytes($byte_size),
phutil_format_bytes($min_size)));
continue;
}
if ($max_size && ($byte_size > $max_size)) {
echo tsprintf(
"%s\n",
pht(
'%s: File size (%s) is larger than maximum size (%s).',
$monogram,
phutil_format_bytes($byte_size),
phutil_format_bytes($max_size)));
continue;
}
if ($is_dry_run) {
echo tsprintf(
"%s\n",
pht(
'%s: (%s) Would migrate from "%s" to "%s" (dry run)...',
$monogram,
phutil_format_bytes($byte_size),
$engine_key,
$target_key));
} else {
echo tsprintf(
"%s\n",
pht(
'%s: (%s) Migrating from "%s" to "%s"...',
$monogram,
phutil_format_bytes($byte_size),
$engine_key,
$target_key));
}
try {
if ($is_dry_run) {
// Do nothing, this is a dry run.
} else {
$file->migrateToEngine($target_engine, $is_copy);
}
$total_files += 1;
$total_bytes += $byte_size;
echo tsprintf(
"%s\n",
pht('Done.'));
} catch (Exception $ex) {
echo tsprintf(
"%s\n",
pht('Failed! %s', (string)$ex));
$failed[] = $file;
throw $ex;
}
}
echo tsprintf(
"%s\n",
pht(
'Total Migrated Files: %s',
new PhutilNumber($total_files)));
echo tsprintf(
"%s\n",
pht(
'Total Migrated Bytes: %s',
phutil_format_bytes($total_bytes)));
if ($is_dry_run) {
echo tsprintf(
"%s\n",
pht(
'This was a dry run, so no real migrations were performed.'));
}
if ($failed) {
$monograms = mpull($failed, 'getMonogram');
echo tsprintf(
"%s\n",
pht('Failures: %s.', implode(', ', $monograms)));
return 1;
}
return 0;
}
}
diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
index 9cbc119c8..983dea4a0 100644
--- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
@@ -1,307 +1,319 @@
<?php
final class PhabricatorEmbedFileRemarkupRule
extends PhabricatorObjectRemarkupRule {
private $viewer;
const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids';
protected function getObjectNamePrefix() {
return 'F';
}
protected function loadObjects(array $ids) {
$engine = $this->getEngine();
$this->viewer = $engine->getConfig('viewer');
$objects = id(new PhabricatorFileQuery())
->setViewer($this->viewer)
->withIDs($ids)
->needTransforms(
array(
PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW,
))
->execute();
$phids_key = self::KEY_EMBED_FILE_PHIDS;
$phids = $engine->getTextMetadata($phids_key, array());
foreach (mpull($objects, 'getPHID') as $phid) {
$phids[] = $phid;
}
$engine->setTextMetadata($phids_key, $phids);
return $objects;
}
protected function renderObjectEmbed(
$object,
PhabricatorObjectHandle $handle,
$options) {
$options = $this->getFileOptions($options) + array(
'name' => $object->getName(),
);
$is_viewable_image = $object->isViewableImage();
$is_audio = $object->isAudio();
$is_video = $object->isVideo();
$force_link = ($options['layout'] == 'link');
// If a file is both audio and video, as with "application/ogg" by default,
// render it as video but allow the user to specify `media=audio` if they
// want to force it to render as audio.
if ($is_audio && $is_video) {
$media = $options['media'];
if ($media == 'audio') {
$is_video = false;
} else {
$is_audio = false;
}
}
$options['viewable'] = ($is_viewable_image || $is_audio || $is_video);
if ($is_viewable_image && !$force_link) {
return $this->renderImageFile($object, $handle, $options);
} else if ($is_video && !$force_link) {
return $this->renderVideoFile($object, $handle, $options);
} else if ($is_audio && !$force_link) {
return $this->renderAudioFile($object, $handle, $options);
} else {
return $this->renderFileLink($object, $handle, $options);
}
}
private function getFileOptions($option_string) {
$options = array(
'size' => null,
'layout' => 'left',
'float' => false,
'width' => null,
'height' => null,
'alt' => null,
'media' => null,
'autoplay' => null,
'loop' => null,
);
if ($option_string) {
$option_string = trim($option_string, ', ');
$parser = new PhutilSimpleOptions();
$options = $parser->parse($option_string) + $options;
}
return $options;
}
private function renderImageFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
require_celerity_resource('phui-lightbox-css');
$attrs = array();
$image_class = 'phabricator-remarkup-embed-image';
$use_size = true;
if (!$options['size']) {
$width = $this->parseDimension($options['width']);
$height = $this->parseDimension($options['height']);
if ($width || $height) {
$use_size = false;
$attrs += array(
'src' => $file->getBestURI(),
'width' => $width,
'height' => $height,
);
}
}
if ($use_size) {
switch ((string)$options['size']) {
case 'full':
$attrs += array(
'src' => $file->getBestURI(),
'height' => $file->getImageHeight(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-full';
break;
// Displays "full" in normal Remarkup, "wide" in Documents
case 'wide':
$attrs += array(
'src' => $file->getBestURI(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-wide';
break;
case 'thumb':
default:
$preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW;
$xform = PhabricatorFileTransform::getTransformByKey($preview_key);
$existing_xform = $file->getTransform($preview_key);
if ($existing_xform) {
$xform_uri = $existing_xform->getCDNURI('data');
} else {
$xform_uri = $file->getURIForTransform($xform);
}
$attrs['src'] = $xform_uri;
$dimensions = $xform->getTransformedDimensions($file);
if ($dimensions) {
list($x, $y) = $dimensions;
$attrs['width'] = $x;
$attrs['height'] = $y;
}
break;
}
}
if (isset($options['alt'])) {
$attrs['alt'] = $options['alt'];
}
$img = phutil_tag('img', $attrs);
$embed = javelin_tag(
'a',
array(
'href' => $file->getBestURI(),
'class' => $image_class,
'sigil' => 'lightboxable',
'meta' => array(
'phid' => $file->getPHID(),
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'viewable' => true,
'monogram' => $file->getMonogram(),
),
),
$img);
switch ($options['layout']) {
case 'right':
case 'center':
case 'inline':
case 'left':
$layout_class = 'phabricator-remarkup-embed-layout-'.$options['layout'];
break;
default:
$layout_class = 'phabricator-remarkup-embed-layout-left';
break;
}
if ($options['float']) {
switch ($options['layout']) {
case 'center':
case 'inline':
break;
case 'right':
$layout_class .= ' phabricator-remarkup-embed-float-right';
break;
case 'left':
default:
$layout_class .= ' phabricator-remarkup-embed-float-left';
break;
}
}
return phutil_tag(
($options['layout'] == 'inline' ? 'span' : 'div'),
array(
'class' => $layout_class,
),
$embed);
}
private function renderAudioFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return $this->renderMediaFile('audio', $file, $handle, $options);
}
private function renderVideoFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return $this->renderMediaFile('video', $file, $handle, $options);
}
private function renderMediaFile(
$tag,
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
$is_video = ($tag == 'video');
if (idx($options, 'autoplay')) {
$preload = 'auto';
$autoplay = 'autoplay';
} else {
// If we don't preload video, the user can't see the first frame and
// has no clue what they're looking at, so always preload.
if ($is_video) {
$preload = 'auto';
} else {
$preload = 'none';
}
$autoplay = null;
}
// Rendering contexts like feed can disable autoplay.
$engine = $this->getEngine();
if ($engine->getConfig('autoplay.disable')) {
$autoplay = null;
}
+ if ($is_video) {
+ // See T13135. Chrome refuses to play videos with type "video/quicktime",
+ // even though it may actually be able to play them. The least awful fix
+ // based on available information is to simply omit the "type" attribute
+ // from `<source />` tags. This causes Chrome to try to play the video
+ // and realize it can, and does not appear to produce any bad behavior in
+ // any other browser.
+ $mime_type = null;
+ } else {
+ $mime_type = $file->getMimeType();
+ }
+
return $this->newTag(
$tag,
array(
'controls' => 'controls',
'preload' => $preload,
'autoplay' => $autoplay,
'loop' => idx($options, 'loop') ? 'loop' : null,
'alt' => $options['alt'],
'class' => 'phabricator-media',
),
$this->newTag(
'source',
array(
'src' => $file->getBestURI(),
- 'type' => $file->getMimeType(),
+ 'type' => $mime_type,
)));
}
private function renderFileLink(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return id(new PhabricatorFileLinkView())
->setViewer($this->viewer)
->setFilePHID($file->getPHID())
->setFileName($this->assertFlatText($options['name']))
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable((bool)$options['viewable'])
->setFileSize(phutil_format_bytes($file->getByteSize()))
->setFileMonogram($file->getMonogram());
}
private function parseDimension($string) {
$string = trim($string);
if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
return $string;
}
return null;
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index b795678cd..5109202a3 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1689 +1,1702 @@
<?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.absolute | Temporary file lifetime as an epoch timestamp.
* | ttl.relative | Temporary file lifetime, relative to now, 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.
* | profile | Marks the file as a profile image.
* | format | Internal encoding format.
* | mime-type | Optional, explicit file MIME type.
* | builtin | Optional filename, identifies this as a builtin.
*
*/
final class PhabricatorFile extends PhabricatorFileDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorIndexableInterface,
PhabricatorNgramsInterface {
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
const METADATA_CAN_CDN = 'canCDN';
const METADATA_BUILTIN = 'builtin';
const METADATA_PARTIAL = 'partial';
const METADATA_PROFILE = 'profile';
const METADATA_STORAGE = 'storage';
const METADATA_INTEGRITY = 'integrity';
const METADATA_CHUNK = 'chunk';
const STATUS_ACTIVE = 'active';
const STATUS_DELETED = 'deleted';
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $metadata = array();
protected $mailKey;
protected $builtinKey;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
protected $ttl;
protected $isExplicitUpload = 1;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $isPartial = 0;
protected $isDeleted = 0;
private $objects = self::ATTACHABLE;
private $objectPHIDs = self::ATTACHABLE;
private $originalFile = self::ATTACHABLE;
private $transforms = 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)
->setIsPartial(0)
->attachOriginalFile(null)
->attachObjects(array())
->attachObjectPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255?',
'mimeType' => 'text255?',
'byteSize' => 'uint64',
'storageEngine' => 'text32',
'storageFormat' => 'text32',
'storageHandle' => 'text255',
'authorPHID' => 'phid?',
'secretKey' => 'bytes20?',
'contentHash' => 'bytes64?',
'ttl' => 'epoch?',
'isExplicitUpload' => 'bool?',
'mailKey' => 'bytes20',
'isPartial' => 'bool',
'builtinKey' => 'text64?',
'isDeleted' => 'bool',
),
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'),
),
'key_partial' => array(
'columns' => array('authorPHID', 'isPartial'),
),
'key_builtin' => array(
'columns' => array('builtinKey'),
'unique' => true,
),
'key_engine' => array(
'columns' => array('storageEngine', 'storageHandle(64)'),
),
),
) + 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 saveAndIndex() {
$this->save();
- PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
+
+ if ($this->isIndexableFile()) {
+ PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
+ }
+
return $this;
}
+ private function isIndexableFile() {
+ if ($this->getIsChunk()) {
+ return false;
+ }
+
+ return true;
+ }
+
public function getMonogram() {
return 'F'.$this->getID();
}
public function scrambleSecret() {
return $this->setSecretKey($this->generateSecretKey());
}
public static function readUploadedFileData($spec) {
if (!$spec) {
throw new Exception(pht('No file was uploaded!'));
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
// NOTE: If we parsed the request body ourselves, the files we wrote will
// not be registered in the `is_uploaded_file()` list. It's fine to skip
// this check: it just protects against sloppy code from the long ago era
// of "register_globals".
if (ini_get('enable_post_data_reading')) {
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
throw new Exception(pht('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(pht('File size disagrees with uploaded size.'));
}
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()) {
return self::newFromFileData($data, $params);
}
public static function newFileFromContentHash($hash, array $params) {
if ($hash === null) {
return null;
}
// Check to see if a file with same hash already exists.
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1',
$hash);
if (!$file) {
return null;
}
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
$copy_of_storage_properties = $file->getStorageProperties();
$copy_of_byte_size = $file->getByteSize();
$copy_of_mime_type = $file->getMimeType();
$new_file = self::initializeNewFile();
$new_file->setByteSize($copy_of_byte_size);
$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->setStorageProperties($copy_of_storage_properties);
$new_file->setMimeType($copy_of_mime_type);
$new_file->copyDimensions($file);
$new_file->readPropertiesFromParameters($params);
$new_file->saveAndIndex();
return $new_file;
}
public static function newChunkedFile(
PhabricatorFileStorageEngine $engine,
$length,
array $params) {
$file = self::initializeNewFile();
$file->setByteSize($length);
// NOTE: Once we receive the first chunk, we'll detect its MIME type and
// update the parent file if a MIME type hasn't been provided. This matters
// for large media files like video.
$mime_type = idx($params, 'mime-type');
if (!strlen($mime_type)) {
$file->setMimeType('application/octet-stream');
}
$chunked_hash = idx($params, 'chunkedHash');
// Get rid of this parameter now; we aren't passing it any further down
// the stack.
unset($params['chunkedHash']);
if ($chunked_hash) {
$file->setContentHash($chunked_hash);
} else {
// See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
// discussion of this.
$seed = Filesystem::readRandomBytes(64);
$hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
$seed);
$file->setContentHash($hash);
}
$file->setStorageEngine($engine->getEngineIdentifier());
$file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
// Chunked files are always stored raw because they do not actually store
// data. The chunks do, and can be individually formatted.
$file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY);
$file->setIsPartial(1);
$file->readPropertiesFromParameters($params);
return $file;
}
private static function buildFromFileData($data, array $params = array()) {
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$size = strlen($data);
$engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
if (!$engines) {
throw new Exception(
pht(
'No configured storage engine can store this file. See '.
'"Configuring File Storage" in the documentation for '.
'information on configuring storage engines.'));
}
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception(pht('No valid storage engines are available!'));
}
$file = self::initializeNewFile();
$aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY;
$has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type);
if ($has_aes !== null) {
$default_key = PhabricatorFileAES256StorageFormat::FORMATKEY;
} else {
$default_key = PhabricatorFileRawStorageFormat::FORMATKEY;
}
$key = idx($params, 'format', $default_key);
// Callers can pass in an object explicitly instead of a key. This is
// primarily useful for unit tests.
if ($key instanceof PhabricatorFileStorageFormat) {
$format = clone $key;
} else {
$format = clone PhabricatorFileStorageFormat::requireFormat($key);
}
$format->setFile($file);
$properties = $format->newStorageProperties();
$file->setStorageFormat($format->getStorageFormatKey());
$file->setStorageProperties($properties);
$data_handle = null;
$engine_identifier = null;
$integrity_hash = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
$result = $file->writeToEngine(
$engine,
$data,
$params);
list($engine_identifier, $data_handle, $integrity_hash) = $result;
// 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(
pht('All storage engines failed to write file:'),
$exceptions);
}
$file->setByteSize(strlen($data));
$hash = self::hashFileContent($data);
$file->setContentHash($hash);
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
$file->setIntegrityHash($integrity_hash);
$file->readPropertiesFromParameters($params);
if (!$file->getMimeType()) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
unset($tmp);
}
try {
$file->updateDimensions(false);
} catch (Exception $ex) {
// Do nothing.
}
$file->saveAndIndex();
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
if ($hash !== null) {
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
}
return self::buildFromFileData($data, $params);
}
public function migrateToEngine(
PhabricatorFileStorageEngine $engine,
$make_copy) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("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, $integrity_hash) = $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->setIntegrityHash($integrity_hash);
$this->save();
if (!$make_copy) {
$this->deleteFileDataIfUnused(
$old_engine,
$old_identifier,
$old_handle);
}
return $this;
}
public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("You can not migrate a file which hasn't yet been saved."));
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
$engine = $this->instantiateStorageEngine();
$old_handle = $this->getStorageHandle();
$properties = $format->newStorageProperties();
$this->setStorageFormat($format->getStorageFormatKey());
$this->setStorageProperties($properties);
- list($identifier, $new_handle) = $this->writeToEngine(
+ list($identifier, $new_handle, $integrity_hash) = $this->writeToEngine(
$engine,
$data,
$params);
$this->setStorageHandle($new_handle);
+ $this->setIntegrityHash($integrity_hash);
$this->save();
$this->deleteFileDataIfUnused(
$engine,
$identifier,
$old_handle);
return $this;
}
public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("You can not cycle keys for a file which hasn't yet been saved."));
}
$properties = $format->cycleStorageProperties();
$this->setStorageProperties($properties);
$this->save();
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$format = $this->newStorageFormat();
$data_iterator = array($data);
$formatted_iterator = $format->newWriteIterator($data_iterator);
$formatted_data = $this->loadDataFromIterator($formatted_iterator);
$integrity_hash = $engine->newIntegrityHash($formatted_data, $format);
$data_handle = $engine->writeFile($formatted_data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
pht(
"Storage engine '%s' executed %s but did not return a valid ".
"handle ('%s') to the data: it must be nonempty and no longer ".
"than 255 characters.",
$engine_class,
'writeFile()',
$data_handle));
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
pht(
"Storage engine '%s' returned an improper engine identifier '{%s}': ".
"it must be nonempty and no longer than 32 characters.",
$engine_class,
$engine_identifier));
}
return array($engine_identifier, $data_handle, $integrity_hash);
}
/**
* Download a remote resource over HTTP and save the response body as a file.
*
* This method respects `security.outbound-blacklist`, and protects against
* HTTP redirection (by manually following "Location" headers and verifying
* each destination). It does not protect against DNS rebinding. See
* discussion in T6755.
*/
public static function newFromFileDownload($uri, array $params = array()) {
$timeout = 5;
$redirects = array();
$current = $uri;
while (true) {
try {
if (count($redirects) > 10) {
throw new Exception(
pht('Too many redirects trying to fetch remote URI.'));
}
$resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
$current,
array(
'http',
'https',
));
list($resolved_uri, $resolved_domain) = $resolved;
$current = new PhutilURI($current);
if ($current->getProtocol() == 'http') {
// For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
$fetch_uri = $resolved_uri;
$fetch_host = $resolved_domain;
} else {
// For HTTPS, we can't: cURL won't verify the SSL certificate if
// the domain has been replaced with an IP. But internal services
// presumably will not have valid certificates for rebindable
// domain names on attacker-controlled domains, so the DNS rebinding
// attack should generally not be possible anyway.
$fetch_uri = $current;
$fetch_host = null;
}
$future = id(new HTTPSFuture($fetch_uri))
->setFollowLocation(false)
->setTimeout($timeout);
if ($fetch_host !== null) {
$future->addHeader('Host', $fetch_host);
}
list($status, $body, $headers) = $future->resolve();
if ($status->isRedirect()) {
// This is an HTTP 3XX status, so look for a "Location" header.
$location = null;
foreach ($headers as $header) {
list($name, $value) = $header;
if (phutil_utf8_strtolower($name) == 'location') {
$location = $value;
break;
}
}
// HTTP 3XX status with no "Location" header, just treat this like
// a normal HTTP error.
if ($location === null) {
throw $status;
}
if (isset($redirects[$location])) {
throw new Exception(
pht('Encountered loop while following redirects.'));
}
$redirects[$location] = $location;
$current = $location;
// We'll fall off the bottom and go try this URI now.
} else if ($status->isError()) {
// This is something other than an HTTP 2XX or HTTP 3XX status, so
// just bail out.
throw $status;
} else {
// This is HTTP 2XX, so use the response body to save the file data.
// Provide a default name based on the URI, truncating it if the URI
// is exceptionally long.
$default_name = basename($uri);
$default_name = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(64)
->truncateString($default_name);
$params = $params + array(
'name' => $default_name,
);
return self::newFromFileData($body, $params);
}
} catch (Exception $ex) {
if ($redirects) {
throw new PhutilProxyException(
pht(
'Failed to fetch remote URI "%s" after following %s redirect(s) '.
'(%s): %s',
$uri,
phutil_count($redirects),
implode(' > ', array_keys($redirects)),
$ex->getMessage()),
$ex);
} else {
throw $ex;
}
}
}
}
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) {
// NOTE: Hashing can fail if the algorithm isn't available in the current
// build of PHP. It's fine if we're unable to generate a content hash:
// it just means we'll store extra data when users upload duplicate files
// instead of being able to deduplicate it.
$hash = hash('sha256', $data, $raw_output = false);
if ($hash === false) {
return null;
}
return $hash;
}
public function loadFileData() {
$iterator = $this->getFileDataIterator();
return $this->loadDataFromIterator($iterator);
}
/**
* Return an iterable which emits file content bytes.
*
* @param int Offset for the start of data.
* @param int Offset for the end of data.
* @return Iterable Iterable object which emits requested data.
*/
public function getFileDataIterator($begin = null, $end = null) {
$engine = $this->instantiateStorageEngine();
$format = $this->newStorageFormat();
$iterator = $engine->getRawFileDataIterator(
$this,
$begin,
$end,
$format);
return $iterator;
}
public function getURI() {
return $this->getInfoURI();
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
pht('You must save a file before you can generate a view URI.'));
}
return $this->getCDNURI('data');
}
public function getCDNURI($request_kind) {
if (($request_kind !== 'data') &&
($request_kind !== 'download')) {
throw new Exception(
pht(
'Unknown file content request kind "%s".',
$request_kind));
}
$name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name);
$parts = array();
$parts[] = 'file';
$parts[] = $request_kind;
// If this is an instanced install, add the instance identifier to the URI.
// Instanced configurations behind a CDN may not be able to control the
// request domain used by the CDN (as with AWS CloudFront). Embedding the
// instance identity in the path allows us to distinguish between requests
// originating from different instances but served through the same CDN.
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $this->getSecretKey();
$parts[] = $this->getPHID();
$parts[] = $name;
$path = '/'.implode('/', $parts);
// If this file is only partially uploaded, we're just going to return a
// local URI to make sure that Ajax works, since the page is inevitably
// going to give us an error back.
if ($this->getIsPartial()) {
return PhabricatorEnv::getURI($path);
} else {
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() {
return $this->getCDNURI('download');
}
public function getURIForTransform(PhabricatorFileTransform $transform) {
return $this->getTransformedURI($transform->getTransformKey());
}
private function getTransformedURI($transform) {
$parts = array();
$parts[] = 'file';
$parts[] = 'xform';
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $transform;
$parts[] = $this->getPHID();
$parts[] = $this->getSecretKey();
$path = implode('/', $parts);
$path = $path.'/';
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 isVideo() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isPDF() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = array(
'application/pdf' => 'application/pdf',
);
$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(pht('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 getDragAndDropDictionary() {
return array(
'id' => $this->getID(),
'phid' => $this->getPHID(),
'uri' => $this->getBestURI(),
);
}
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(
pht(
"Storage engine '%s' could not be located!",
$engine_identifier));
}
public static function buildAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFileStorageEngine')
->execute();
}
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, 'fa-file-o');
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
public function setStorageProperties(array $properties) {
$this->metadata[self::METADATA_STORAGE] = $properties;
return $this;
}
public function getStorageProperties() {
return idx($this->metadata, self::METADATA_STORAGE, array());
}
public function getStorageProperty($key, $default = null) {
$properties = $this->getStorageProperties();
return idx($properties, $key, $default);
}
public function loadDataFromIterator($iterator) {
$result = '';
foreach ($iterator as $chunk) {
$result .= $chunk;
}
return $result;
}
public function updateDimensions($save = true) {
if (!$this->isViewableImage()) {
throw new Exception(pht('This file is not a viewable image.'));
}
if (!function_exists('imagecreatefromstring')) {
throw new Exception(pht('Cannot retrieve image information.'));
}
if ($this->getIsChunk()) {
throw new Exception(
pht('Refusing to assess image dimensions of file chunk.'));
}
$engine = $this->instantiateStorageEngine();
if ($engine->isChunkEngine()) {
throw new Exception(
pht('Refusing to assess image dimensions of chunked file.'));
}
$data = $this->loadFileData();
$img = @imagecreatefromstring($data);
if ($img === false) {
throw new Exception(pht('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<PhabricatorFilesBuiltinFile> List of builtin file specs.
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
*/
public static function loadBuiltins(PhabricatorUser $user, array $builtins) {
$builtins = mpull($builtins, null, 'getBuiltinFileKey');
// NOTE: Anyone is allowed to access builtin files.
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuiltinKeys(array_keys($builtins))
->execute();
$results = array();
foreach ($files as $file) {
$builtin_key = $file->getBuiltinName();
if ($builtin_key !== null) {
$results[$builtin_key] = $file;
}
}
$build = array();
foreach ($builtins as $key => $builtin) {
if (isset($results[$key])) {
continue;
}
$data = $builtin->loadBuiltinFileData();
$params = array(
'name' => $builtin->getBuiltinDisplayName(),
'canCDN' => true,
'builtin' => $key,
);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$file = self::newFromFileData($data, $params);
} catch (AphrontDuplicateKeyQueryException $ex) {
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuiltinKeys(array($key))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Collided mid-air when generating builtin file "%s", but '.
'then failed to load the object we collided with.',
$key));
}
}
unset($unguarded);
$file->attachObjectPHIDs(array());
$file->attachObjects(array());
$results[$key] = $file;
}
return $results;
}
/**
* 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) {
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
->setName($name);
$key = $builtin->getBuiltinFileKey();
return idx(self::loadBuiltins($user, array($builtin)), $key);
}
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;
}
public function isBuiltin() {
return ($this->getBuiltinName() !== null);
}
public function getBuiltinName() {
return idx($this->metadata, self::METADATA_BUILTIN);
}
public function setBuiltinName($name) {
$this->metadata[self::METADATA_BUILTIN] = $name;
return $this;
}
public function getIsProfileImage() {
return idx($this->metadata, self::METADATA_PROFILE);
}
public function setIsProfileImage($value) {
$this->metadata[self::METADATA_PROFILE] = $value;
return $this;
}
public function getIsChunk() {
return idx($this->metadata, self::METADATA_CHUNK);
}
public function setIsChunk($value) {
$this->metadata[self::METADATA_CHUNK] = $value;
return $this;
}
public function setIntegrityHash($integrity_hash) {
$this->metadata[self::METADATA_INTEGRITY] = $integrity_hash;
return $this;
}
public function getIntegrityHash() {
return idx($this->metadata, self::METADATA_INTEGRITY);
}
public function newIntegrityHash() {
$engine = $this->instantiateStorageEngine();
if ($engine->isChunkEngine()) {
return null;
}
$format = $this->newStorageFormat();
$storage_handle = $this->getStorageHandle();
$data = $engine->readFile($storage_handle);
return $engine->newIntegrityHash($data, $format);
}
/**
* 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 = PhabricatorObjectHasFileEdgeType::EDGECONST;
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 = PhabricatorObjectHasFileEdgeType::EDGECONST;
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) {
PhutilTypeSpec::checkMap(
$params,
array(
'name' => 'optional string',
'authorPHID' => 'optional string',
'ttl.relative' => 'optional int',
'ttl.absolute' => 'optional int',
'viewPolicy' => 'optional string',
'isExplicitUpload' => 'optional bool',
'canCDN' => 'optional bool',
'profile' => 'optional bool',
'format' => 'optional string|PhabricatorFileStorageFormat',
'mime-type' => 'optional string',
'builtin' => 'optional string',
'storageEngines' => 'optional list<PhabricatorFileStorageEngine>',
'chunk' => 'optional bool',
));
$file_name = idx($params, 'name');
$this->setName($file_name);
$author_phid = idx($params, 'authorPHID');
$this->setAuthorPHID($author_phid);
$absolute_ttl = idx($params, 'ttl.absolute');
$relative_ttl = idx($params, 'ttl.relative');
if ($absolute_ttl !== null && $relative_ttl !== null) {
throw new Exception(
pht(
'Specify an absolute TTL or a relative TTL, but not both.'));
} else if ($absolute_ttl !== null) {
if ($absolute_ttl < PhabricatorTime::getNow()) {
throw new Exception(
pht(
'Absolute TTL must be in the present or future, but TTL "%s" '.
'is in the past.',
$absolute_ttl));
}
$this->setTtl($absolute_ttl);
} else if ($relative_ttl !== null) {
if ($relative_ttl < 0) {
throw new Exception(
pht(
'Relative TTL must be zero or more seconds, but "%s" is '.
'negative.',
$relative_ttl));
}
$max_relative = phutil_units('365 days in seconds');
if ($relative_ttl > $max_relative) {
throw new Exception(
pht(
'Relative TTL must not be more than "%s" seconds, but TTL '.
'"%s" was specified.',
$max_relative,
$relative_ttl));
}
$absolute_ttl = PhabricatorTime::getNow() + $relative_ttl;
$this->setTtl($absolute_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);
}
$builtin = idx($params, 'builtin');
if ($builtin) {
$this->setBuiltinName($builtin);
$this->setBuiltinKey($builtin);
}
$profile = idx($params, 'profile');
if ($profile) {
$this->setIsProfileImage(true);
}
$mime_type = idx($params, 'mime-type');
if ($mime_type) {
$this->setMimeType($mime_type);
}
$is_chunk = idx($params, 'chunk');
if ($is_chunk) {
$this->setIsChunk(true);
}
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);
}
public function newDownloadResponse() {
// We're cheating a little bit here and relying on the fact that
// getDownloadURI() always returns a fully qualified URI with a complete
// domain.
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setCloseDialogBeforeRedirect(true)
->setURI($this->getDownloadURI());
}
public function attachTransforms(array $map) {
$this->transforms = $map;
return $this;
}
public function getTransform($key) {
return $this->assertAttachedKey($this->transforms, $key);
}
public function newStorageFormat() {
$key = $this->getStorageFormat();
$template = PhabricatorFileStorageFormat::requireFormat($key);
$format = id(clone $template)
->setFile($this);
return $format;
}
/* -( 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:
if ($this->isBuiltin()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->getIsProfileImage()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
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);
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the file.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dataURI')
->setType('string')
->setDescription(pht('Download URI for the file data.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('size')
->setType('int')
->setDescription(pht('File size, in bytes.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'dataURI' => $this->getCDNURI('data'),
'size' => (int)$this->getByteSize(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorFileNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/guides/module/PhabricatorGuideInstallModule.php b/src/applications/guides/module/PhabricatorGuideInstallModule.php
index 7a46b2002..172ff7a17 100644
--- a/src/applications/guides/module/PhabricatorGuideInstallModule.php
+++ b/src/applications/guides/module/PhabricatorGuideInstallModule.php
@@ -1,179 +1,179 @@
<?php
final class PhabricatorGuideInstallModule extends PhabricatorGuideModule {
public function getModuleKey() {
return 'install';
}
public function getModuleName() {
return pht('Install Phabricator');
}
public function getModulePosition() {
return 20;
}
public function getIsModuleEnabled() {
if (PhabricatorEnv::getEnvConfig('cluster.instance')) {
return false;
}
return true;
}
public function renderModuleStatus(AphrontRequest $request) {
$viewer = $request->getViewer();
$guide_items = new PhabricatorGuideListView();
$title = pht('Resolve Setup Issues');
$issues_resolved = !PhabricatorSetupCheck::getOpenSetupIssueKeys();
$href = PhabricatorEnv::getURI('/config/issue/');
if ($issues_resolved) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've resolved (or ignored) all outstanding setup issues.");
} else {
$icon = 'fa-warning';
$icon_bg = 'bg-red';
$description =
pht('You have some unresolved setup issues to take care of.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
$title = pht('Login and Registration');
$href = PhabricatorEnv::getURI('/auth/');
$have_auth = (bool)$configs;
if ($have_auth) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've configured at least one authentication provider.");
} else {
$icon = 'fa-key';
$icon_bg = 'bg-sky';
$description = pht(
'Authentication providers allow users to register accounts and '.
'log in to Phabricator.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Configure Phabricator');
$href = PhabricatorEnv::getURI('/config/');
// Just load any config value at all; if one exists the install has figured
// out how to configure things.
$have_config = (bool)id(new PhabricatorConfigEntry())->loadAllWhere(
'1 = 1 LIMIT 1');
if ($have_config) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've configured at least one setting from the web interface.");
} else {
$icon = 'fa-sliders';
$icon_bg = 'bg-sky';
$description = pht(
'Learn how to configure mail and other options in Phabricator.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('User Account Settings');
$href = PhabricatorEnv::getURI('/settings/');
$preferences = id(new PhabricatorUserPreferencesQuery())
->setViewer($viewer)
->withUsers(array($viewer))
->executeOne();
$have_settings = ($preferences && $preferences->getPreferences());
if ($have_settings) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've adjusted at least one setting on your account.");
} else {
$icon = 'fa-wrench';
$icon_bg = 'bg-sky';
$description = pht(
'Configure account settings for all users, or just yourself');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Notification Server');
$href = PhabricatorEnv::getURI('/config/edit/notification.servers/');
$have_notifications = PhabricatorEnv::getEnvConfig('notification.servers');
if ($have_notifications) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've set up a real-time notification server.");
} else {
$icon = 'fa-bell';
$icon_bg = 'bg-sky';
$description = pht(
'Phabricator can deliver notifications in real-time with WebSockets.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$intro = pht(
'Phabricator has been successfully installed. These next guides will '.
'take you through configuration and new user orientation. '.
'These steps are optional, and you can go through them in any order. '.
'If you want to get back to this guide later on, you can find it in '.
'{icon globe} **Applications** under {icon map-o} **Guides**.');
$intro = new PHUIRemarkupView($viewer, $intro);
- $intro = id(new PHUIDocumentViewPro())
+ $intro = id(new PHUIDocumentView())
->appendChild($intro);
return array($intro, $guide_items);
}
}
diff --git a/src/applications/guides/module/PhabricatorGuideQuickStartModule.php b/src/applications/guides/module/PhabricatorGuideQuickStartModule.php
index c06e320df..8c4b4c260 100644
--- a/src/applications/guides/module/PhabricatorGuideQuickStartModule.php
+++ b/src/applications/guides/module/PhabricatorGuideQuickStartModule.php
@@ -1,216 +1,216 @@
<?php
final class PhabricatorGuideQuickStartModule extends PhabricatorGuideModule {
public function getModuleKey() {
return 'quickstart';
}
public function getModuleName() {
return pht('Quick Start');
}
public function getModulePosition() {
return 30;
}
public function getIsModuleEnabled() {
return true;
}
public function renderModuleStatus(AphrontRequest $request) {
$viewer = $request->getViewer();
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
$guide_items = new PhabricatorGuideListView();
$title = pht('Create a Repository');
$repository_check = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->execute();
$href = PhabricatorEnv::getURI('/diffusion/');
if ($repository_check) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've created at least one repository.");
} else {
$icon = 'fa-code';
$icon_bg = 'bg-sky';
$description =
pht('If you are here for code review, let\'s set up your first '.
'repository.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Create a Project');
$project_check = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->execute();
$href = PhabricatorEnv::getURI('/project/');
if ($project_check) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've created at least one project.");
} else {
$icon = 'fa-briefcase';
$icon_bg = 'bg-sky';
$description =
pht('Project tags define everything. Create them for teams, tags, '.
'or actual projects.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Create a Task');
$task_check = id(new ManiphestTaskQuery())
->setViewer($viewer)
->execute();
$href = PhabricatorEnv::getURI('/maniphest/');
if ($task_check) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've created at least one task.");
} else {
$icon = 'fa-anchor';
$icon_bg = 'bg-sky';
$description =
pht('Create some work for the interns in Maniphest.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Build a Dashboard');
$have_dashboard = (bool)PhabricatorDashboardInstall::getDashboard(
$viewer,
PhabricatorHomeApplication::DASHBOARD_DEFAULT,
'PhabricatorHomeApplication');
$href = PhabricatorEnv::getURI('/dashboard/');
if ($have_dashboard) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
"You've created at least one dashboard.");
} else {
$icon = 'fa-dashboard';
$icon_bg = 'bg-sky';
$description =
pht('Customize the default homepage layout and items.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Personalize your Install');
$wordmark = PhabricatorEnv::getEnvConfig('ui.logo');
$href = PhabricatorEnv::getURI('/config/edit/ui.logo/');
if ($wordmark) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
'It looks amazing, good work. Home Sweet Home.');
} else {
$icon = 'fa-home';
$icon_bg = 'bg-sky';
$description =
pht('Change the name and add your company logo, just to give it a '.
'little extra polish.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
$title = pht('Explore Applications');
$href = PhabricatorEnv::getURI('/applications/');
$icon = 'fa-globe';
$icon_bg = 'bg-sky';
$description =
pht('See all the applications included in Phabricator.');
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
if (!$instance) {
$title = pht('Invite Collaborators');
$people_check = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->execute();
$people = count($people_check);
$href = PhabricatorEnv::getURI('/people/invite/send/');
if ($people > 1) {
$icon = 'fa-check';
$icon_bg = 'bg-green';
$description = pht(
'Your invitations have been accepted. You will not be alone on '.
'this journey.');
} else {
$icon = 'fa-group';
$icon_bg = 'bg-sky';
$description =
pht('Invite the rest of your team to get started on Phabricator.');
}
$item = id(new PhabricatorGuideItemView())
->setTitle($title)
->setHref($href)
->setIcon($icon)
->setIconBackground($icon_bg)
->setDescription($description);
$guide_items->addItem($item);
}
$intro = pht(
'If you\'re new to Phabricator, these optional steps can help you learn '.
'the basics. Conceptually, Phabricator is structured as a graph, and '.
'repositories, tasks, and projects are all independent from each other. '.
'Feel free to set up Phabricator for how you work best, and explore '.
'these features at your own pace.');
$intro = new PHUIRemarkupView($viewer, $intro);
- $intro = id(new PHUIDocumentViewPro())
+ $intro = id(new PHUIDocumentView())
->appendChild($intro);
return array($intro, $guide_items);
}
}
diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
index dc63b5fd9..7437e48f6 100644
--- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
+++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
@@ -1,204 +1,217 @@
<?php
final class HarbormasterBuildStatus extends Phobject {
const STATUS_INACTIVE = 'inactive';
const STATUS_PENDING = 'pending';
const STATUS_BUILDING = 'building';
const STATUS_PASSED = 'passed';
const STATUS_FAILED = 'failed';
const STATUS_ABORTED = 'aborted';
const STATUS_ERROR = 'error';
const STATUS_PAUSED = 'paused';
const STATUS_DEADLOCKED = 'deadlocked';
private $key;
private $properties;
public function __construct($key, array $properties) {
$this->key = $key;
$this->properties = $properties;
}
public static function newBuildStatusObject($status) {
$spec = self::getBuildStatusSpec($status);
return new self($status, $spec);
}
private function getProperty($key) {
if (!array_key_exists($key, $this->properties)) {
throw new Exception(
pht(
'Attempting to access unknown build status property ("%s").',
$key));
}
return $this->properties[$key];
}
public function isBuilding() {
return $this->getProperty('isBuilding');
}
public function isPaused() {
return ($this->key === self::STATUS_PAUSED);
}
public function isComplete() {
return $this->getProperty('isComplete');
}
public function isPassed() {
return ($this->key === self::STATUS_PASSED);
}
/**
* Get a human readable name for a build status constant.
*
* @param const Build status constant.
* @return string Human-readable name.
*/
public static function getBuildStatusName($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['name'];
}
public static function getBuildStatusMap() {
$specs = self::getBuildStatusSpecMap();
return ipull($specs, 'name');
}
public static function getBuildStatusIcon($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['icon'];
}
public static function getBuildStatusColor($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['color'];
}
public static function getBuildStatusANSIColor($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['color.ansi'];
}
public static function getWaitingStatusConstants() {
return array(
self::STATUS_INACTIVE,
self::STATUS_PENDING,
);
}
public static function getActiveStatusConstants() {
return array(
self::STATUS_BUILDING,
self::STATUS_PAUSED,
);
}
+ public static function getIncompleteStatusConstants() {
+ $map = self::getBuildStatusSpecMap();
+
+ $constants = array();
+ foreach ($map as $constant => $spec) {
+ if (!$spec['isComplete']) {
+ $constants[] = $constant;
+ }
+ }
+
+ return $constants;
+ }
+
public static function getCompletedStatusConstants() {
return array(
self::STATUS_PASSED,
self::STATUS_FAILED,
self::STATUS_ABORTED,
self::STATUS_ERROR,
self::STATUS_DEADLOCKED,
);
}
private static function getBuildStatusSpecMap() {
return array(
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'icon' => 'fa-circle-o',
'color' => 'dark',
'color.ansi' => 'yellow',
'isBuilding' => false,
'isComplete' => false,
),
self::STATUS_PENDING => array(
'name' => pht('Pending'),
'icon' => 'fa-circle-o',
'color' => 'blue',
'color.ansi' => 'yellow',
'isBuilding' => true,
'isComplete' => false,
),
self::STATUS_BUILDING => array(
'name' => pht('Building'),
'icon' => 'fa-chevron-circle-right',
'color' => 'blue',
'color.ansi' => 'yellow',
'isBuilding' => true,
'isComplete' => false,
),
self::STATUS_PASSED => array(
'name' => pht('Passed'),
'icon' => 'fa-check-circle',
'color' => 'green',
'color.ansi' => 'green',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_FAILED => array(
'name' => pht('Failed'),
'icon' => 'fa-times-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_ABORTED => array(
'name' => pht('Aborted'),
'icon' => 'fa-minus-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_ERROR => array(
'name' => pht('Unexpected Error'),
'icon' => 'fa-minus-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_PAUSED => array(
'name' => pht('Paused'),
'icon' => 'fa-minus-circle',
'color' => 'dark',
'color.ansi' => 'yellow',
'isBuilding' => false,
'isComplete' => false,
),
self::STATUS_DEADLOCKED => array(
'name' => pht('Deadlocked'),
'icon' => 'fa-exclamation-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
);
}
private static function getBuildStatusSpec($status) {
$map = self::getBuildStatusSpecMap();
if (isset($map[$status])) {
return $map[$status];
}
return array(
'name' => pht('Unknown ("%s")', $status),
'icon' => 'fa-question-circle',
'color' => 'bluegrey',
'color.ansi' => 'magenta',
'isBuilding' => false,
'isComplete' => false,
);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
index ddab677fa..843ffd470 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
@@ -1,162 +1,151 @@
<?php
final class HarbormasterBuildActionController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$via = $request->getURIData('via');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$can_issue = $build->canRestartBuild();
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$can_issue = $build->canPauseBuild();
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$can_issue = $build->canResumeBuild();
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$can_issue = $build->canAbortBuild();
break;
default:
return new Aphront400Response();
}
$build->assertCanIssueCommand($viewer, $action);
switch ($via) {
case 'buildable':
$return_uri = '/'.$build->getBuildable()->getMonogram();
break;
default:
$return_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
break;
}
if ($request->isDialogFormPost() && $can_issue) {
- $editor = id(new HarbormasterBuildTransactionEditor())
- ->setActor($viewer)
- ->setContentSourceFromRequest($request)
- ->setContinueOnNoEffect(true)
- ->setContinueOnMissingFields(true);
-
- $xaction = id(new HarbormasterBuildTransaction())
- ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
- ->setNewValue($action);
-
- $editor->applyTransactions($build, array($xaction));
-
+ $build->sendMessage($viewer, $action);
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($can_issue) {
$title = pht('Really restart build?');
$body = pht(
'Progress on this build will be discarded and the build will '.
'restart. Side effects of the build will occur again. Really '.
'restart build?');
$submit = pht('Restart Build');
} else {
$title = pht('Unable to Restart Build');
if ($build->isRestarting()) {
$body = pht(
'This build is already restarting. You can not reissue a '.
'restart command to a restarting build.');
} else {
$body = pht('You can not restart this build.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
if ($can_issue) {
$title = pht('Really abort build?');
$body = pht(
'Progress on this build will be discarded. Really '.
'abort build?');
$submit = pht('Abort Build');
} else {
$title = pht('Unable to Abort Build');
$body = pht('You can not abort this build.');
}
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
if ($can_issue) {
$title = pht('Really pause build?');
$body = pht(
'If you pause this build, work will halt once the current steps '.
'complete. You can resume the build later.');
$submit = pht('Pause Build');
} else {
$title = pht('Unable to Pause Build');
if ($build->isComplete()) {
$body = pht(
'This build is already complete. You can not pause a completed '.
'build.');
} else if ($build->isPaused()) {
$body = pht(
'This build is already paused. You can not pause a build which '.
'has already been paused.');
} else if ($build->isPausing()) {
$body = pht(
'This build is already pausing. You can not reissue a pause '.
'command to a pausing build.');
} else {
$body = pht(
'This build can not be paused.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
if ($can_issue) {
$title = pht('Really resume build?');
$body = pht(
'Work will continue on the build. Really resume?');
$submit = pht('Resume Build');
} else {
$title = pht('Unable to Resume Build');
if ($build->isResuming()) {
$body = pht(
'This build is already resuming. You can not reissue a resume '.
'command to a resuming build.');
} else if (!$build->isPaused()) {
$body = pht(
'This build is not paused. You can only resume a paused '.
'build.');
}
}
break;
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild($body)
->addCancelButton($return_uri);
if ($can_issue) {
$dialog->addSubmitButton($submit);
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php
index 40313cdaf..ee02b2563 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php
@@ -1,50 +1,54 @@
<?php
final class HarbormasterBuildLogDownloadController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $request->getURIData('id');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$cancel_uri = $log->getURI();
$file_phid = $log->getFilePHID();
if (!$file_phid) {
return $this->newDialog()
->setTitle(pht('Log Not Finalized'))
->appendParagraph(
pht(
'Logs must be fully written and processed before they can be '.
'downloaded. This log is still being written or processed.'))
->addCancelButton($cancel_uri, pht('Wait Patiently'));
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
return $this->newDialog()
->setTitle(pht('Unable to Load File'))
->appendParagraph(
pht(
'Unable to load the file for this log. The file may have been '.
'destroyed.'))
->addCancelButton($cancel_uri);
}
return $file->newDownloadResponse();
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
index 7a5050bec..0f1f2bc02 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
@@ -1,893 +1,897 @@
<?php
final class HarbormasterBuildLogRenderController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$highlight_range = $request->getURILineRange('lines', 1000);
$log_size = $this->getTotalByteLength($log);
$head_lines = $request->getInt('head');
if ($head_lines === null) {
$head_lines = 8;
}
$head_lines = min($head_lines, 1024);
$head_lines = max($head_lines, 0);
$tail_lines = $request->getInt('tail');
if ($tail_lines === null) {
$tail_lines = 16;
}
$tail_lines = min($tail_lines, 1024);
$tail_lines = max($tail_lines, 0);
$head_offset = $request->getInt('headOffset');
if ($head_offset === null) {
$head_offset = 0;
}
$tail_offset = $request->getInt('tailOffset');
if ($tail_offset === null) {
$tail_offset = $log_size;
}
// Figure out which ranges we're actually going to read. We'll read either
// one range (either just at the head, or just at the tail) or two ranges
// (one at the head and one at the tail).
// This gets a little bit tricky because: the ranges may overlap; we just
// want to do one big read if there is only a little bit of text left
// between the ranges; we may not know where the tail range ends; and we
// can only read forward from line map markers, not from any arbitrary
// position in the file.
$bytes_per_line = 140;
$body_lines = 8;
$views = array();
if ($head_lines > 0) {
$views[] = array(
'offset' => $head_offset,
'lines' => $head_lines,
'direction' => 1,
'limit' => $tail_offset,
);
}
if ($highlight_range) {
$highlight_views = $this->getHighlightViews(
$log,
$highlight_range,
$log_size);
foreach ($highlight_views as $highlight_view) {
$views[] = $highlight_view;
}
}
if ($tail_lines > 0) {
$views[] = array(
'offset' => $tail_offset,
'lines' => $tail_lines,
'direction' => -1,
'limit' => $head_offset,
);
}
$reads = $views;
foreach ($reads as $key => $read) {
$offset = $read['offset'];
$lines = $read['lines'];
$read_length = 0;
$read_length += ($lines * $bytes_per_line);
$read_length += ($body_lines * $bytes_per_line);
$direction = $read['direction'];
if ($direction < 0) {
if ($offset > $read_length) {
$offset -= $read_length;
} else {
$read_length = $offset;
$offset = 0;
}
}
$position = $log->getReadPosition($offset);
list($position_offset, $position_line) = $position;
$read_length += ($offset - $position_offset);
$reads[$key]['fetchOffset'] = $position_offset;
$reads[$key]['fetchLength'] = $read_length;
$reads[$key]['fetchLine'] = $position_line;
}
$reads = $this->mergeOverlappingReads($reads);
foreach ($reads as $key => $read) {
$fetch_offset = $read['fetchOffset'];
$fetch_length = $read['fetchLength'];
if ($fetch_offset + $fetch_length > $log_size) {
$fetch_length = $log_size - $fetch_offset;
}
$data = $log->loadData($fetch_offset, $fetch_length);
$offset = $read['fetchOffset'];
$line = $read['fetchLine'];
$lines = $this->getLines($data);
$line_data = array();
foreach ($lines as $line_text) {
$length = strlen($line_text);
$line_data[] = array(
'offset' => $offset,
'length' => $length,
'line' => $line,
'data' => $line_text,
);
$line += 1;
$offset += $length;
}
$reads[$key]['data'] = $data;
$reads[$key]['lines'] = $line_data;
}
foreach ($views as $view_key => $view) {
$anchor_byte = $view['offset'];
if ($view['direction'] < 0) {
$anchor_byte = $anchor_byte - 1;
}
$data_key = null;
foreach ($reads as $read_key => $read) {
$s = $read['fetchOffset'];
$e = $s + $read['fetchLength'];
if (($s <= $anchor_byte) && ($e >= $anchor_byte)) {
$data_key = $read_key;
break;
}
}
if ($data_key === null) {
throw new Exception(
pht('Unable to find fetch!'));
}
$anchor_key = null;
foreach ($reads[$data_key]['lines'] as $line_key => $line) {
$s = $line['offset'];
$e = $s + $line['length'];
if (($s <= $anchor_byte) && ($e > $anchor_byte)) {
$anchor_key = $line_key;
break;
}
}
if ($anchor_key === null) {
throw new Exception(
pht(
'Unable to find lines.'));
}
if ($view['direction'] > 0) {
$slice_offset = $anchor_key;
} else {
$slice_offset = max(0, $anchor_key - ($view['lines'] - 1));
}
$slice_length = $view['lines'];
$views[$view_key] += array(
'sliceKey' => $data_key,
'sliceOffset' => $slice_offset,
'sliceLength' => $slice_length,
);
}
foreach ($views as $view_key => $view) {
$slice_key = $view['sliceKey'];
$lines = array_slice(
$reads[$slice_key]['lines'],
$view['sliceOffset'],
$view['sliceLength']);
$data_offset = null;
$data_length = null;
foreach ($lines as $line) {
if ($data_offset === null) {
$data_offset = $line['offset'];
}
$data_length += $line['length'];
}
// If the view cursor starts in the middle of a line, we're going to
// strip part of the line.
$direction = $view['direction'];
if ($direction > 0) {
$view_offset = $view['offset'];
$view_length = $data_length;
if ($data_offset < $view_offset) {
$trim = ($view_offset - $data_offset);
$view_length -= $trim;
}
$limit = $view['limit'];
if ($limit !== null) {
if ($limit < ($view_offset + $view_length)) {
$view_length = ($limit - $view_offset);
}
}
} else {
$view_offset = $data_offset;
$view_length = $data_length;
if ($data_offset + $data_length > $view['offset']) {
$view_length -= (($data_offset + $data_length) - $view['offset']);
}
$limit = $view['limit'];
if ($limit !== null) {
if ($limit > $view_offset) {
$view_length -= ($limit - $view_offset);
$view_offset = $limit;
}
}
}
$views[$view_key] += array(
'viewOffset' => $view_offset,
'viewLength' => $view_length,
);
}
$views = $this->mergeOverlappingViews($views);
foreach ($views as $view_key => $view) {
$slice_key = $view['sliceKey'];
$lines = array_slice(
$reads[$slice_key]['lines'],
$view['sliceOffset'],
$view['sliceLength']);
$view_offset = $view['viewOffset'];
foreach ($lines as $line_key => $line) {
$line_offset = $line['offset'];
if ($line_offset >= $view_offset) {
break;
}
$trim = ($view_offset - $line_offset);
if ($trim && ($trim >= strlen($line['data']))) {
unset($lines[$line_key]);
continue;
}
$line_data = substr($line['data'], $trim);
$lines[$line_key]['data'] = $line_data;
$lines[$line_key]['length'] = strlen($line_data);
$lines[$line_key]['offset'] += $trim;
break;
}
$view_end = $view['viewOffset'] + $view['viewLength'];
foreach ($lines as $line_key => $line) {
$line_end = $line['offset'] + $line['length'];
if ($line_end <= $view_end) {
continue;
}
$trim = ($line_end - $view_end);
if ($trim && ($trim >= strlen($line['data']))) {
unset($lines[$line_key]);
continue;
}
$line_data = substr($line['data'], -$trim);
$lines[$line_key]['data'] = $line_data;
$lines[$line_key]['length'] = strlen($line_data);
}
$views[$view_key]['viewData'] = $lines;
}
$spacer = null;
$render = array();
$head_view = head($views);
if ($head_view['viewOffset'] > $head_offset) {
$render[] = array(
'spacer' => true,
'head' => $head_offset,
'tail' => $head_view['viewOffset'],
);
}
foreach ($views as $view) {
if ($spacer) {
$spacer['tail'] = $view['viewOffset'];
$render[] = $spacer;
}
$render[] = $view;
$spacer = array(
'spacer' => true,
'head' => ($view['viewOffset'] + $view['viewLength']),
);
}
$tail_view = last($views);
if ($tail_view['viewOffset'] + $tail_view['viewLength'] < $tail_offset) {
$render[] = array(
'spacer' => true,
'head' => $tail_view['viewOffset'] + $tail_view['viewLength'],
'tail' => $tail_offset,
);
}
$uri = $log->getURI();
$rows = array();
foreach ($render as $range) {
if (isset($range['spacer'])) {
$rows[] = $this->renderExpandRow($range);
continue;
}
$lines = $range['viewData'];
foreach ($lines as $line) {
$display_line = ($line['line'] + 1);
$display_text = ($line['data']);
$row_attr = array();
if ($highlight_range) {
if (($display_line >= $highlight_range[0]) &&
($display_line <= $highlight_range[1])) {
$row_attr = array(
'class' => 'phabricator-source-highlight',
);
}
}
$display_line = phutil_tag(
'a',
array(
'href' => $uri.'$'.$display_line,
'data-n' => $display_line,
),
'');
$line_cell = phutil_tag('th', array(), $display_line);
$text_cell = phutil_tag('td', array(), $display_text);
$rows[] = phutil_tag(
'tr',
$row_attr,
array(
$line_cell,
$text_cell,
));
}
}
if ($log->getLive()) {
$last_view = last($views);
$last_line = last($last_view['viewData']);
if ($last_line) {
$last_offset = $last_line['offset'];
} else {
$last_offset = 0;
}
$last_tail = $last_view['viewOffset'] + $last_view['viewLength'];
$show_live = ($last_tail === $log_size);
if ($show_live) {
$rows[] = $this->renderLiveRow($last_offset);
}
}
$table = javelin_tag(
'table',
array(
'class' => 'harbormaster-log-table PhabricatorMonospaced',
'sigil' => 'phabricator-source',
'meta' => array(
'uri' => $log->getURI(),
),
),
$rows);
// When this is a normal AJAX request, return the rendered log fragment
// in an AJAX payload.
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'markup' => hsprintf('%s', $table),
));
}
// If the page is being accessed as a standalone page, present a
// readable version of the fragment for debugging.
require_celerity_resource('harbormaster-css');
$header = pht('Standalone Log Fragment');
$render_view = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeaderText($header)
->appendChild($table);
$page_view = id(new PHUITwoColumnView())
->setFooter($render_view);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI())
->addTextCrumb(pht('Fragment'))
->setBorder(true);
return $this->newPage()
->setTitle(
array(
pht('Build Log %d', $log->getID()),
pht('Standalone Fragment'),
))
->setCrumbs($crumbs)
->appendChild($page_view);
}
private function getTotalByteLength(HarbormasterBuildLog $log) {
$total_bytes = $log->getByteLength();
if ($total_bytes) {
return (int)$total_bytes;
}
// TODO: Remove this after enough time has passed for installs to run
// log rebuilds or decide they don't care about older logs.
// Older logs don't have this data denormalized onto the log record unless
// an administrator has run `bin/harbormaster rebuild-log --all` or
// similar. Try to figure it out by summing up the size of each chunk.
// Note that the log may also be legitimately empty and have actual size
// zero.
$chunk = new HarbormasterBuildLogChunk();
$conn = $chunk->establishConnection('r');
$row = queryfx_one(
$conn,
'SELECT SUM(size) total FROM %T WHERE logID = %d',
$chunk->getTableName(),
$log->getID());
return (int)$row['total'];
}
private function getLines($data) {
$parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE);
if (last($parts) === '') {
array_pop($parts);
}
$lines = array();
for ($ii = 0; $ii < count($parts); $ii += 2) {
$line = $parts[$ii];
if (isset($parts[$ii + 1])) {
$line .= $parts[$ii + 1];
}
$lines[] = $line;
}
return $lines;
}
private function mergeOverlappingReads(array $reads) {
// Find planned reads which will overlap and merge them into a single
// larger read.
$uk = array_keys($reads);
$vk = array_keys($reads);
foreach ($uk as $ukey) {
foreach ($vk as $vkey) {
// Don't merge a range into itself, even though they do technically
// overlap.
if ($ukey === $vkey) {
continue;
}
$uread = idx($reads, $ukey);
if ($uread === null) {
continue;
}
$vread = idx($reads, $vkey);
if ($vread === null) {
continue;
}
$us = $uread['fetchOffset'];
$ue = $us + $uread['fetchLength'];
$vs = $vread['fetchOffset'];
$ve = $vs + $vread['fetchLength'];
if (($vs > $ue) || ($ve < $us)) {
continue;
}
$min = min($us, $vs);
$max = max($ue, $ve);
$reads[$ukey]['fetchOffset'] = $min;
$reads[$ukey]['fetchLength'] = ($max - $min);
$reads[$ukey]['fetchLine'] = min(
$uread['fetchLine'],
$vread['fetchLine']);
unset($reads[$vkey]);
}
}
return $reads;
}
private function mergeOverlappingViews(array $views) {
$uk = array_keys($views);
$vk = array_keys($views);
$body_lines = 8;
$body_bytes = ($body_lines * 140);
foreach ($uk as $ukey) {
foreach ($vk as $vkey) {
if ($ukey === $vkey) {
continue;
}
$uview = idx($views, $ukey);
if ($uview === null) {
continue;
}
$vview = idx($views, $vkey);
if ($vview === null) {
continue;
}
// If these views don't use the same line data, don't try to
// merge them.
if ($uview['sliceKey'] != $vview['sliceKey']) {
continue;
}
// If these views are overlapping or separated by only a few bytes,
// merge them into a single view.
$us = $uview['viewOffset'];
$ue = $us + $uview['viewLength'];
$vs = $vview['viewOffset'];
$ve = $vs + $vview['viewLength'];
// Don't merge if one of the slices starts at a byte offset
// significantly after the other ends.
if (($vs > $ue + $body_bytes) || ($us > $ve + $body_bytes)) {
continue;
}
$uss = $uview['sliceOffset'];
$use = $uss + $uview['sliceLength'];
$vss = $vview['sliceOffset'];
$vse = $vss + $vview['sliceLength'];
// Don't merge if one of the slices starts at a line offset
// significantly after the other ends.
if ($uss > ($vse + $body_lines) || $vss > ($use + $body_lines)) {
continue;
}
// These views are overlapping or nearly overlapping, so we merge
// them. We merge views even if they aren't exactly adjacent since
// it's silly to render an "expand more" which only expands a couple
// of lines.
$offset = min($us, $vs);
$length = max($ue, $ve) - $offset;
$slice_offset = min($uss, $vss);
$slice_length = max($use, $vse) - $slice_offset;
$views[$ukey] = array(
'viewOffset' => $offset,
'viewLength' => $length,
'sliceOffset' => $slice_offset,
'sliceLength' => $slice_length,
) + $views[$ukey];
unset($views[$vkey]);
}
}
return $views;
}
private function renderExpandRow($range) {
$icon_up = id(new PHUIIconView())
->setIcon('fa-chevron-up');
$icon_down = id(new PHUIIconView())
->setIcon('fa-chevron-down');
$up_text = array(
pht('Show More Above'),
' ',
$icon_up,
);
$expand_up = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'meta' => array(
'headOffset' => $range['head'],
'tailOffset' => $range['tail'],
'head' => 128,
'tail' => 0,
),
),
$up_text);
$mid_text = pht(
'Show More (%s Bytes)',
new PhutilNumber($range['tail'] - $range['head']));
$expand_mid = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'meta' => array(
'headOffset' => $range['head'],
'tailOffset' => $range['tail'],
'head' => 128,
'tail' => 128,
),
),
$mid_text);
$down_text = array(
$icon_down,
' ',
pht('Show More Below'),
);
$expand_down = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'meta' => array(
'headOffset' => $range['head'],
'tailOffset' => $range['tail'],
'head' => 0,
'tail' => 128,
),
),
$down_text);
$expand_cells = array(
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-up',
),
$expand_up),
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-mid',
),
$expand_mid),
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-down',
),
$expand_down),
);
return $this->renderActionTable($expand_cells);
}
private function renderLiveRow($log_size) {
$icon_down = id(new PHUIIconView())
->setIcon('fa-angle-double-down');
$icon_pause = id(new PHUIIconView())
->setIcon('fa-pause');
$follow = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand harbormaster-log-live',
'class' => 'harbormaster-log-follow-start',
'meta' => array(
'headOffset' => $log_size,
'head' => 0,
'tail' => 1024,
'live' => true,
),
),
array(
$icon_down,
' ',
pht('Follow Log'),
));
$stop_following = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'class' => 'harbormaster-log-follow-stop',
'meta' => array(
'stop' => true,
),
),
array(
$icon_pause,
' ',
pht('Stop Following Log'),
));
$expand_cells = array(
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-follow',
),
array(
$follow,
$stop_following,
)),
);
return $this->renderActionTable($expand_cells);
}
private function renderActionTable(array $action_cells) {
$action_row = phutil_tag('tr', array(), $action_cells);
$action_table = phutil_tag(
'table',
array(
'class' => 'harbormaster-log-expand-table',
),
$action_row);
$format_cells = array(
phutil_tag('th', array()),
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-cell',
),
$action_table),
);
return phutil_tag('tr', array(), $format_cells);
}
private function getHighlightViews(
HarbormasterBuildLog $log,
array $range,
$log_size) {
// If we're highlighting a line range in the file, we first need to figure
// out the offsets for the lines we care about.
list($range_min, $range_max) = $range;
// Read the markers to find a range we can load which includes both lines.
$read_range = $log->getLineSpanningRange($range_min, $range_max);
list($min_pos, $max_pos, $min_line) = $read_range;
$length = ($max_pos - $min_pos);
// Reject to do the read if it requires us to examine a huge amount of
// data. For example, the user may request lines "$1-1000" of a file where
// each line has 100MB of text.
$limit = (1024 * 1024 * 16);
if ($length > $limit) {
return array();
}
$data = $log->loadData($min_pos, $length);
$offset = $min_pos;
$min_offset = null;
$max_offset = null;
$lines = $this->getLines($data);
$number = ($min_line + 1);
foreach ($lines as $line) {
if ($min_offset === null) {
if ($number === $range_min) {
$min_offset = $offset;
}
}
$offset += strlen($line);
if ($max_offset === null) {
if ($number === $range_max) {
$max_offset = $offset;
break;
}
}
$number += 1;
}
$context_lines = 8;
// Build views around the beginning and ends of the respective lines. We
// expect these views to overlap significantly in normal circumstances
// and be merged later.
$views = array();
if ($min_offset !== null) {
$views[] = array(
'offset' => $min_offset,
'lines' => $context_lines + ($range_max - $range_min) - 1,
'direction' => 1,
'limit' => null,
);
if ($min_offset > 0) {
$views[] = array(
'offset' => $min_offset,
'lines' => $context_lines,
'direction' => -1,
'limit' => null,
);
}
}
if ($max_offset !== null) {
$views[] = array(
'offset' => $max_offset,
'lines' => $context_lines + ($range_max - $range_min),
'direction' => -1,
'limit' => null,
);
if ($max_offset < $log_size) {
$views[] = array(
'offset' => $max_offset,
'lines' => $context_lines,
'direction' => 1,
'limit' => null,
);
}
}
return $views;
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
index 888b3bddb..0128dad0b 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
@@ -1,51 +1,55 @@
<?php
final class HarbormasterBuildLogViewController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$target = $log->getBuildTarget();
$build = $target->getBuild();
$page_title = pht('Build Log %d', $log->getID());
$log_view = id(new HarbormasterBuildLogView())
->setViewer($viewer)
->setBuildLog($log)
->setHighlightedLineRange($request->getURIData('lines'))
->setEnableHighlighter(true);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Build Logs'))
->addTextCrumb(
pht('Build %d', $build->getID()),
$build->getURI())
->addTextCrumb($page_title)
->setBorder(true);
$page_header = id(new PHUIHeaderView())
->setHeader($page_title);
$page_view = id(new PHUITwoColumnView())
->setHeader($page_header)
->setFooter($log_view);
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->appendChild($page_view);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
index ac3a7101f..b0f2654ea 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -1,776 +1,780 @@
<?php
final class HarbormasterBuildViewController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $request->getURIData('id');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
require_celerity_resource('harbormaster-css');
$title = pht('Build %d', $id);
$warnings = array();
$page_header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($build)
->setHeaderIcon('fa-cubes');
$is_restarting = $build->isRestarting();
if ($is_restarting) {
$page_header->setStatus(
'fa-exclamation-triangle', 'red', pht('Restarting'));
} else if ($build->isPausing()) {
$page_header->setStatus(
'fa-exclamation-triangle', 'red', pht('Pausing'));
} else if ($build->isResuming()) {
$page_header->setStatus(
'fa-exclamation-triangle', 'red', pht('Resuming'));
} else if ($build->isAborting()) {
$page_header->setStatus(
'fa-exclamation-triangle', 'red', pht('Aborting'));
}
$max_generation = (int)$build->getBuildGeneration();
if ($max_generation === 0) {
$min_generation = 0;
} else {
$min_generation = 1;
}
if ($is_restarting) {
$max_generation = $max_generation + 1;
}
$generation = $request->getURIData('generation');
if ($generation === null) {
$generation = $max_generation;
} else {
$generation = (int)$generation;
}
if ($generation < $min_generation || $generation > $max_generation) {
return new Aphront404Response();
}
if ($generation < $max_generation) {
$warnings[] = pht(
'You are viewing an older run of this build. %s',
phutil_tag(
'a',
array(
'href' => $build->getURI(),
),
pht('View Current Build')));
}
$curtain = $this->buildCurtainView($build);
$properties = $this->buildPropertyList($build);
$history = $this->buildHistoryTable(
$build,
$generation,
$min_generation,
$max_generation);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $build->getBuildable());
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$build_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->needBuildSteps(true)
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($generation))
->execute();
if ($build_targets) {
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(mpull($build_targets, 'getPHID'))
->execute();
$messages = mgroup($messages, 'getReceiverPHID');
} else {
$messages = array();
}
if ($build_targets) {
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(mpull($build_targets, 'getPHID'))
->execute();
$artifacts = msort($artifacts, 'getArtifactKey');
$artifacts = mgroup($artifacts, 'getBuildTargetPHID');
} else {
$artifacts = array();
}
$targets = array();
foreach ($build_targets as $build_target) {
$header = id(new PHUIHeaderView())
->setHeader($build_target->getName())
->setUser($viewer)
->setHeaderIcon('fa-bullseye');
$target_box = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeader($header);
$tab_group = new PHUITabGroupView();
$target_box->addTabGroup($tab_group);
$property_list = new PHUIPropertyListView();
$target_artifacts = idx($artifacts, $build_target->getPHID(), array());
$links = array();
$type_uri = HarbormasterURIArtifact::ARTIFACTCONST;
foreach ($target_artifacts as $artifact) {
if ($artifact->getArtifactType() == $type_uri) {
$impl = $artifact->getArtifactImplementation();
if ($impl->isExternalLink()) {
$links[] = $impl->renderLink();
}
}
}
if ($links) {
$links = phutil_implode_html(phutil_tag('br'), $links);
$property_list->addProperty(
pht('External Link'),
$links);
}
$status_view = new PHUIStatusListView();
$item = new PHUIStatusItemView();
$status = $build_target->getTargetStatus();
$status_name =
HarbormasterBuildTarget::getBuildTargetStatusName($status);
$icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status);
$color = HarbormasterBuildTarget::getBuildTargetStatusColor($status);
$item->setTarget($status_name);
$item->setIcon($icon, $color);
$status_view->addItem($item);
$when = array();
$started = $build_target->getDateStarted();
$now = PhabricatorTime::getNow();
if ($started) {
$ended = $build_target->getDateCompleted();
if ($ended) {
$when[] = pht(
'Completed at %s',
phabricator_datetime($ended, $viewer));
$duration = ($ended - $started);
if ($duration) {
$when[] = pht(
'Built for %s',
phutil_format_relative_time_detailed($duration));
} else {
$when[] = pht('Built instantly');
}
} else {
$when[] = pht(
'Started at %s',
phabricator_datetime($started, $viewer));
$duration = ($now - $started);
if ($duration) {
$when[] = pht(
'Running for %s',
phutil_format_relative_time_detailed($duration));
}
}
} else {
$created = $build_target->getDateCreated();
$when[] = pht(
'Queued at %s',
phabricator_datetime($started, $viewer));
$duration = ($now - $created);
if ($duration) {
$when[] = pht(
'Waiting for %s',
phutil_format_relative_time_detailed($duration));
}
}
$property_list->addProperty(
pht('When'),
phutil_implode_html(" \xC2\xB7 ", $when));
$property_list->addProperty(pht('Status'), $status_view);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Overview'))
->setKey('overview')
->appendChild($property_list));
$step = $build_target->getBuildStep();
if ($step) {
$description = $step->getDescription();
if ($description) {
$description = new PHUIRemarkupView($viewer, $description);
$property_list->addSectionHeader(
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
$property_list->addTextContent($description);
}
} else {
$target_box->setFormErrors(
array(
pht(
'This build step has since been deleted on the build plan. '.
'Some information may be omitted.'),
));
}
$details = $build_target->getDetails();
$property_list = new PHUIPropertyListView();
foreach ($details as $key => $value) {
$property_list->addProperty($key, $value);
}
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Configuration'))
->setKey('configuration')
->appendChild($property_list));
$variables = $build_target->getVariables();
$variables_tab = $this->buildProperties($variables);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Variables'))
->setKey('variables')
->appendChild($variables_tab));
$artifacts_tab = $this->buildArtifacts($build_target, $target_artifacts);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Artifacts'))
->setKey('artifacts')
->appendChild($artifacts_tab));
$build_messages = idx($messages, $build_target->getPHID(), array());
$messages_tab = $this->buildMessages($build_messages);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Messages'))
->setKey('messages')
->appendChild($messages_tab));
$property_list = new PHUIPropertyListView();
$property_list->addProperty(
pht('Build Target ID'),
$build_target->getID());
$property_list->addProperty(
pht('Build Target PHID'),
$build_target->getPHID());
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Metadata'))
->setKey('metadata')
->appendChild($property_list));
$targets[] = $target_box;
$targets[] = $this->buildLog($build, $build_target, $generation);
}
$timeline = $this->buildTransactionTimeline(
$build,
new HarbormasterBuildTransactionQuery());
$timeline->setShouldTerminate(true);
if ($warnings) {
$warnings = id(new PHUIInfoView())
->setErrors($warnings)
->setSeverity(PHUIInfoView::SEVERITY_WARNING);
} else {
$warnings = null;
}
$view = id(new PHUITwoColumnView())
->setHeader($page_header)
->setCurtain($curtain)
->setMainColumn(
array(
$warnings,
$properties,
$history,
$targets,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildArtifacts(
HarbormasterBuildTarget $build_target,
array $artifacts) {
$viewer = $this->getViewer();
$rows = array();
foreach ($artifacts as $artifact) {
$impl = $artifact->getArtifactImplementation();
if ($impl) {
$summary = $impl->renderArtifactSummary($viewer);
$type_name = $impl->getArtifactTypeName();
} else {
$summary = pht('<Unknown Artifact Type>');
$type_name = $artifact->getType();
}
$rows[] = array(
$artifact->getArtifactKey(),
$type_name,
$summary,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('This target has no associated artifacts.'))
->setHeaders(
array(
pht('Key'),
pht('Type'),
pht('Summary'),
))
->setColumnClasses(
array(
'pri',
'',
'wide',
));
return $table;
}
private function buildLog(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target,
$generation) {
$request = $this->getRequest();
$viewer = $request->getUser();
$limit = $request->getInt('l', 25);
$logs = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(array($build_target->getPHID()))
->execute();
$empty_logs = array();
$log_boxes = array();
foreach ($logs as $log) {
$start = 1;
$lines = preg_split("/\r\n|\r|\n/", $log->getLogText());
if ($limit !== 0) {
$start = count($lines) - $limit;
if ($start >= 1) {
$lines = array_slice($lines, -$limit, $limit);
} else {
$start = 1;
}
}
$id = null;
$is_empty = false;
if (count($lines) === 1 && trim($lines[0]) === '') {
// Prevent Harbormaster from showing empty build logs.
$id = celerity_generate_unique_node_id();
$empty_logs[] = $id;
$is_empty = true;
}
$log_view = new ShellLogView();
$log_view->setLines($lines);
$log_view->setStart($start);
$subheader = $this->createLogHeader($build, $log, $limit, $generation);
$prototype_view = id(new PHUIButtonView())
->setTag('a')
->setHref($log->getURI())
->setIcon('fa-file-text-o')
->setText(pht('New View (Prototype)'));
$header = id(new PHUIHeaderView())
->setHeader(pht(
'Build Log %d (%s - %s)',
$log->getID(),
$log->getLogSource(),
$log->getLogType()))
->addActionLink($prototype_view)
->setSubheader($subheader)
->setUser($viewer);
$log_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($log_view);
if ($is_empty) {
$log_box = phutil_tag(
'div',
array(
'style' => 'display: none',
'id' => $id,
),
$log_box);
}
$log_boxes[] = $log_box;
}
if ($empty_logs) {
$hide_id = celerity_generate_unique_node_id();
Javelin::initBehavior('phabricator-reveal-content');
$expand = phutil_tag(
'div',
array(
'id' => $hide_id,
'class' => 'harbormaster-empty-logs-are-hidden',
),
array(
pht(
'%s empty logs are hidden.',
phutil_count($empty_logs)),
' ',
javelin_tag(
'a',
array(
'href' => '#',
'sigil' => 'reveal-content',
'meta' => array(
'showIDs' => $empty_logs,
'hideIDs' => array($hide_id),
),
),
pht('Show all logs.')),
));
array_unshift($log_boxes, $expand);
}
return $log_boxes;
}
private function createLogHeader($build, $log, $limit, $generation) {
$options = array(
array(
'n' => 25,
),
array(
'n' => 50,
),
array(
'n' => 100,
),
array(
'n' => 0,
'label' => pht('Unlimited'),
),
);
$base_uri = id(new PhutilURI($build->getURI().$generation.'/'));
$links = array();
foreach ($options as $option) {
$n = $option['n'];
$label = idx($option, 'label', $n);
$is_selected = ($limit == $n);
if ($is_selected) {
$links[] = phutil_tag(
'strong',
array(),
$label);
} else {
$links[] = phutil_tag(
'a',
array(
'href' => (string)$base_uri->alter('l', $n),
),
$label);
}
}
return phutil_tag(
'span',
array(),
array(
phutil_implode_html(' - ', $links),
' ',
pht('Lines'),
));
}
private function buildCurtainView(HarbormasterBuild $build) {
$viewer = $this->getViewer();
$id = $build->getID();
$curtain = $this->newCurtainView($build);
$can_restart =
$build->canRestartBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_RESTART);
$can_pause =
$build->canPauseBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_PAUSE);
$can_resume =
$build->canResumeBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_RESUME);
$can_abort =
$build->canAbortBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_ABORT);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Restart Build'))
->setIcon('fa-repeat')
->setHref($this->getApplicationURI('/build/restart/'.$id.'/'))
->setDisabled(!$can_restart)
->setWorkflow(true));
if ($build->canResumeBuild()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Resume Build'))
->setIcon('fa-play')
->setHref($this->getApplicationURI('/build/resume/'.$id.'/'))
->setDisabled(!$can_resume)
->setWorkflow(true));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Pause Build'))
->setIcon('fa-pause')
->setHref($this->getApplicationURI('/build/pause/'.$id.'/'))
->setDisabled(!$can_pause)
->setWorkflow(true));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Abort Build'))
->setIcon('fa-exclamation-triangle')
->setHref($this->getApplicationURI('/build/abort/'.$id.'/'))
->setDisabled(!$can_abort)
->setWorkflow(true));
return $curtain;
}
private function buildPropertyList(HarbormasterBuild $build) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array(
$build->getBuildablePHID(),
$build->getBuildPlanPHID(),
))
->execute();
$properties->addProperty(
pht('Buildable'),
$handles[$build->getBuildablePHID()]->renderLink());
$properties->addProperty(
pht('Build Plan'),
$handles[$build->getBuildPlanPHID()]->renderLink());
$properties->addProperty(
pht('Status'),
$this->getStatus($build));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildHistoryTable(
HarbormasterBuild $build,
$generation,
$min_generation,
$max_generation) {
if ($max_generation === $min_generation) {
return null;
}
$viewer = $this->getViewer();
$uri = $build->getURI();
$rows = array();
$rowc = array();
for ($ii = $max_generation; $ii >= $min_generation; $ii--) {
if ($generation == $ii) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$rows[] = array(
phutil_tag(
'a',
array(
'href' => $uri.$ii.'/',
),
pht('Run %d', $ii)),
);
}
$table = id(new AphrontTableView($rows))
->setColumnClasses(
array(
'pri wide',
))
->setRowClasses($rowc);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('History'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
private function getStatus(HarbormasterBuild $build) {
$status_view = new PHUIStatusListView();
$item = new PHUIStatusItemView();
if ($build->isPausing()) {
$status_name = pht('Pausing');
$icon = PHUIStatusItemView::ICON_RIGHT;
$color = 'dark';
} else {
$status = $build->getBuildStatus();
$status_name =
HarbormasterBuildStatus::getBuildStatusName($status);
$icon = HarbormasterBuildStatus::getBuildStatusIcon($status);
$color = HarbormasterBuildStatus::getBuildStatusColor($status);
}
$item->setTarget($status_name);
$item->setIcon($icon, $color);
$status_view->addItem($item);
return $status_view;
}
private function buildMessages(array $messages) {
$viewer = $this->getRequest()->getUser();
if ($messages) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(mpull($messages, 'getAuthorPHID'))
->execute();
} else {
$handles = array();
}
$rows = array();
foreach ($messages as $message) {
$rows[] = array(
$message->getID(),
$handles[$message->getAuthorPHID()]->renderLink(),
$message->getType(),
$message->getIsConsumed() ? pht('Consumed') : null,
phabricator_datetime($message->getDateCreated(), $viewer),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht('No messages for this build target.'));
$table->setHeaders(
array(
pht('ID'),
pht('From'),
pht('Type'),
pht('Consumed'),
pht('Received'),
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
'date',
));
return $table;
}
private function buildProperties(array $properties) {
ksort($properties);
$rows = array();
foreach ($properties as $key => $value) {
$rows[] = array(
$key,
$value,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Value'),
))
->setColumnClasses(
array(
'pri right',
'wide',
));
return $table;
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
index 58e5cf8c4..1e79ad2b4 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
@@ -1,355 +1,358 @@
<?php
final class HarbormasterBuildableViewController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
// Pull builds and build targets.
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($buildable->getPHID()))
->needBuildTargets(true)
->execute();
list($lint, $unit) = $this->renderLintAndUnit($buildable, $builds);
$buildable->attachBuilds($builds);
$object = $buildable->getBuildableObject();
$build_list = $this->buildBuildList($buildable);
$title = pht('Buildable %d', $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($buildable)
->setStatus(
$buildable->getStatusIcon(),
$buildable->getStatusColor(),
$buildable->getStatusDisplayName())
->setHeaderIcon('fa-recycle');
$timeline = $this->buildTransactionTimeline(
$buildable,
new HarbormasterBuildableTransactionQuery());
$timeline->setShouldTerminate(true);
$curtain = $this->buildCurtainView($buildable);
$properties = $this->buildPropertyList($buildable);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($buildable->getMonogram());
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$properties,
$lint,
$unit,
$build_list,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtainView(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$id = $buildable->getID();
$curtain = $this->newCurtainView($buildable);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$buildable,
PhabricatorPolicyCapability::CAN_EDIT);
$can_restart = false;
$can_resume = false;
$can_pause = false;
$can_abort = false;
$command_restart = HarbormasterBuildCommand::COMMAND_RESTART;
$command_resume = HarbormasterBuildCommand::COMMAND_RESUME;
$command_pause = HarbormasterBuildCommand::COMMAND_PAUSE;
$command_abort = HarbormasterBuildCommand::COMMAND_ABORT;
foreach ($buildable->getBuilds() as $build) {
if ($build->canRestartBuild()) {
if ($build->canIssueCommand($viewer, $command_restart)) {
$can_restart = true;
}
}
if ($build->canResumeBuild()) {
if ($build->canIssueCommand($viewer, $command_resume)) {
$can_resume = true;
}
}
if ($build->canPauseBuild()) {
if ($build->canIssueCommand($viewer, $command_pause)) {
$can_pause = true;
}
}
if ($build->canAbortBuild()) {
if ($build->canIssueCommand($viewer, $command_abort)) {
$can_abort = true;
}
}
}
$restart_uri = "buildable/{$id}/restart/";
$pause_uri = "buildable/{$id}/pause/";
$resume_uri = "buildable/{$id}/resume/";
$abort_uri = "buildable/{$id}/abort/";
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-repeat')
->setName(pht('Restart All Builds'))
->setHref($this->getApplicationURI($restart_uri))
->setWorkflow(true)
->setDisabled(!$can_restart || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pause')
->setName(pht('Pause All Builds'))
->setHref($this->getApplicationURI($pause_uri))
->setWorkflow(true)
->setDisabled(!$can_pause || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-play')
->setName(pht('Resume All Builds'))
->setHref($this->getApplicationURI($resume_uri))
->setWorkflow(true)
->setDisabled(!$can_resume || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-exclamation-triangle')
->setName(pht('Abort All Builds'))
->setHref($this->getApplicationURI($abort_uri))
->setWorkflow(true)
->setDisabled(!$can_abort || !$can_edit));
return $curtain;
}
private function buildPropertyList(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$container_phid = $buildable->getContainerPHID();
$buildable_phid = $buildable->getBuildablePHID();
if ($container_phid) {
$properties->addProperty(
pht('Container'),
$viewer->renderHandle($container_phid));
}
$properties->addProperty(
pht('Buildable'),
$viewer->renderHandle($buildable_phid));
$properties->addProperty(
pht('Origin'),
$buildable->getIsManualBuildable()
? pht('Manual Buildable')
: pht('Automatic Buildable'));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildBuildList(HarbormasterBuildable $buildable) {
$viewer = $this->getRequest()->getUser();
$build_list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($buildable->getBuilds() as $build) {
$view_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Build %d', $build->getID()))
->setHeader($build->getName())
->setHref($view_uri);
$status = $build->getBuildStatus();
$status_color = HarbormasterBuildStatus::getBuildStatusColor($status);
$status_name = HarbormasterBuildStatus::getBuildStatusName($status);
$item->setStatusIcon('fa-dot-circle-o '.$status_color, $status_name);
$item->addAttribute($status_name);
if ($build->isRestarting()) {
$item->addIcon('fa-repeat', pht('Restarting'));
} else if ($build->isPausing()) {
$item->addIcon('fa-pause', pht('Pausing'));
} else if ($build->isResuming()) {
$item->addIcon('fa-play', pht('Resuming'));
}
$build_id = $build->getID();
$restart_uri = "build/restart/{$build_id}/buildable/";
$resume_uri = "build/resume/{$build_id}/buildable/";
$pause_uri = "build/pause/{$build_id}/buildable/";
$abort_uri = "build/abort/{$build_id}/buildable/";
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-repeat')
->setName(pht('Restart'))
->setHref($this->getApplicationURI($restart_uri))
->setWorkflow(true)
->setDisabled(!$build->canRestartBuild()));
if ($build->canResumeBuild()) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-play')
->setName(pht('Resume'))
->setHref($this->getApplicationURI($resume_uri))
->setWorkflow(true));
} else {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pause')
->setName(pht('Pause'))
->setHref($this->getApplicationURI($pause_uri))
->setWorkflow(true)
->setDisabled(!$build->canPauseBuild()));
}
$targets = $build->getBuildTargets();
if ($targets) {
$target_list = id(new PHUIStatusListView());
foreach ($targets as $target) {
$status = $target->getTargetStatus();
$icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status);
$color = HarbormasterBuildTarget::getBuildTargetStatusColor($status);
$status_name =
HarbormasterBuildTarget::getBuildTargetStatusName($status);
$name = $target->getName();
$target_list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $status_name)
->setTarget(pht('Target %d', $target->getID()))
->setNote($name));
}
$target_box = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_SMALL)
->appendChild($target_list);
$item->appendChild($target_box);
}
$build_list->addItem($item);
}
$build_list->setFlush(true);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Builds'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($build_list);
return $box;
}
private function renderLintAndUnit(
HarbormasterBuildable $buildable,
array $builds) {
$viewer = $this->getViewer();
$targets = array();
foreach ($builds as $build) {
foreach ($build->getBuildTargets() as $target) {
$targets[] = $target;
}
}
if (!$targets) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
if ($lint_data) {
$lint_table = id(new HarbormasterLintPropertyView())
- ->setUser($viewer)
+ ->setViewer($viewer)
->setLimit(10)
->setLintMessages($lint_data);
$lint_href = $this->getApplicationURI('lint/'.$buildable->getID().'/');
$lint_header = id(new PHUIHeaderView())
->setHeader(pht('Lint Messages'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($lint_href)
->setIcon('fa-list-ul')
->setText('View All'));
$lint = id(new PHUIObjectBoxView())
->setHeader($lint_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($lint_table);
} else {
$lint = null;
}
if ($unit_data) {
$unit = id(new HarbormasterUnitSummaryView())
+ ->setViewer($viewer)
->setBuildable($buildable)
->setUnitMessages($unit_data)
->setShowViewAll(true)
->setLimit(5);
} else {
$unit = null;
}
return array($lint, $unit);
}
-
-
}
diff --git a/src/applications/harbormaster/controller/HarbormasterLintMessagesController.php b/src/applications/harbormaster/controller/HarbormasterLintMessagesController.php
index e7636dbbd..8fe1edbca 100644
--- a/src/applications/harbormaster/controller/HarbormasterLintMessagesController.php
+++ b/src/applications/harbormaster/controller/HarbormasterLintMessagesController.php
@@ -1,71 +1,75 @@
<?php
final class HarbormasterLintMessagesController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needBuilds(true)
->needTargets(true)
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$lint_data = array();
if ($target_phids) {
$lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
} else {
$lint_data = array();
}
$lint_table = id(new HarbormasterLintPropertyView())
->setUser($viewer)
->setLintMessages($lint_data);
$lint = id(new PHUIObjectBoxView())
->setHeaderText(pht('Lint Messages'))
->appendChild($lint_table);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(pht('Lint'));
$crumbs->setBorder(true);
$title = array(
$buildable->getMonogram(),
pht('Lint'),
);
$header = id(new PHUIHeaderView())
->setHeader($title);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$lint,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
index 28bd16456..6ebadf7a6 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
@@ -1,440 +1,444 @@
<?php
final class HarbormasterPlanViewController extends HarbormasterPlanController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
$timeline = $this->buildTransactionTimeline(
$plan,
new HarbormasterBuildPlanTransactionQuery());
$timeline->setShouldTerminate(true);
$title = $plan->getName();
$header = id(new PHUIHeaderView())
->setHeader($plan->getName())
->setUser($viewer)
->setPolicyObject($plan)
->setHeaderIcon('fa-ship');
$curtain = $this->buildCurtainView($plan);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Plan %d', $id));
$crumbs->setBorder(true);
list($step_list, $has_any_conflicts, $would_deadlock) =
$this->buildStepList($plan);
$error = null;
if ($would_deadlock) {
$error = pht('This build plan will deadlock when executed, due to '.
'circular dependencies present in the build plan. '.
'Examine the step list and resolve the deadlock.');
} else if ($has_any_conflicts) {
// A deadlocking build will also cause all the artifacts to be
// invalid, so we just skip showing this message if that's the
// case.
$error = pht('This build plan has conflicts in one or more build steps. '.
'Examine the step list and resolve the listed errors.');
}
if ($error) {
$error = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($error);
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$error,
$step_list,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildStepList(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$run_order = HarbormasterBuildGraph::determineDependencyExecution($plan);
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$step_list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('This build plan does not have any build steps yet.'));
$i = 1;
$last_depth = 0;
$has_any_conflicts = false;
$is_deadlocking = false;
foreach ($run_order as $run_ref) {
$step = $steps[$run_ref['node']->getPHID()];
$depth = $run_ref['depth'] + 1;
if ($last_depth !== $depth) {
$last_depth = $depth;
$i = 1;
} else {
$i++;
}
$step_id = $step->getID();
$view_uri = $this->getApplicationURI("step/view/{$step_id}/");
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader($step->getName())
->setHref($view_uri);
$step_list->addItem($item);
$implementation = null;
try {
$implementation = $step->getStepImplementation();
} catch (Exception $ex) {
// We can't initialize the implementation. This might be because
// it's been renamed or no longer exists.
$item
->setStatusIcon('fa-warning red')
->addAttribute(pht(
'This step has an invalid implementation (%s).',
$step->getClassName()));
continue;
}
$item->addAttribute($implementation->getDescription());
$item->setHref($view_uri);
$depends = $step->getStepImplementation()->getDependencies($step);
$inputs = $step->getStepImplementation()->getArtifactInputs();
$outputs = $step->getStepImplementation()->getArtifactOutputs();
$has_conflicts = false;
if ($depends || $inputs || $outputs) {
$available_artifacts =
HarbormasterBuildStepImplementation::getAvailableArtifacts(
$plan,
$step,
null);
$available_artifacts = ipull($available_artifacts, 'type');
list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
$depends,
pht('Depends On'),
$steps);
list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
$inputs,
'in',
pht('Input Artifacts'),
$available_artifacts);
list($outputs_ui) = $this->buildArtifactList(
$outputs,
'out',
pht('Output Artifacts'),
array());
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-io',
),
array(
$depends_ui,
$inputs_ui,
$outputs_ui,
)));
}
if ($has_conflicts) {
$has_any_conflicts = true;
$item->setStatusIcon('fa-warning red');
}
if ($run_ref['cycle']) {
$is_deadlocking = true;
}
if ($is_deadlocking) {
$item->setStatusIcon('fa-warning red');
}
}
$step_list->setFlush(true);
$plan_id = $plan->getID();
$header = id(new PHUIHeaderView())
->setHeader(pht('Build Steps'))
->addActionLink(
id(new PHUIButtonView())
->setText(pht('Add Build Step'))
->setHref($this->getApplicationURI("step/add/{$plan_id}/"))
->setTag('a')
->setIcon('fa-plus')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$step_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($step_list);
return array($step_box, $has_any_conflicts, $is_deadlocking);
}
private function buildCurtainView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$id = $plan->getID();
$curtain = $this->newCurtainView($plan);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Plan'))
->setHref($this->getApplicationURI("plan/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit)
->setIcon('fa-pencil'));
if ($plan->isDisabled()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Enable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-check'));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Disable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-ban'));
}
$can_run = ($can_edit && $plan->canRunManually());
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Run Plan Manually'))
->setHref($this->getApplicationURI("plan/run/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_run)
->setIcon('fa-play-circle'));
$curtain->addPanel(
id(new PHUICurtainPanelView())
->setHeaderText(pht('Created'))
->appendChild(phabricator_datetime($plan->getDateCreated(), $viewer)));
return $curtain;
}
private function buildArtifactList(
array $artifacts,
$kind,
$name,
array $available_artifacts) {
$has_conflicts = false;
if (!$artifacts) {
return array(null, $has_conflicts);
}
$this->requireResource('harbormaster-css');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$is_input = ($kind == 'in');
$list = new PHUIStatusListView();
foreach ($artifacts as $artifact) {
$error = null;
$key = idx($artifact, 'key');
if (!strlen($key)) {
$bound = phutil_tag('em', array(), pht('(null)'));
if ($is_input) {
// This is an unbound input. For now, all inputs are always required.
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Required Input');
$has_conflicts = true;
$error = pht('This input is required, but not configured.');
} else {
// This is an unnamed output. Outputs do not necessarily need to be
// named.
$icon = PHUIStatusItemView::ICON_OPEN;
$color = 'bluegrey';
$icon_label = pht('Unused Output');
}
} else {
$bound = phutil_tag('strong', array(), $key);
if ($is_input) {
if (isset($available_artifacts[$key])) {
if ($available_artifacts[$key] == idx($artifact, 'type')) {
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
} else {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Bad Input Type');
$has_conflicts = true;
$error = pht(
'This input is bound to the wrong artifact type. It is bound '.
'to a "%s" artifact, but should be bound to a "%s" artifact.',
$available_artifacts[$key],
idx($artifact, 'type'));
}
} else {
$icon = PHUIStatusItemView::ICON_QUESTION;
$color = 'red';
$icon_label = pht('Unknown Input');
$has_conflicts = true;
$error = pht(
'This input is bound to an artifact ("%s") which does not exist '.
'at this stage in the build process.',
$key);
}
} else {
$icon = PHUIStatusItemView::ICON_DOWN;
$color = 'green';
$icon_label = pht('Valid Output');
}
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget($artifact['name'])
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
private function buildDependsOnList(
array $step_phids,
$name,
array $steps) {
$has_conflicts = false;
if (count($step_phids) === 0) {
return null;
}
$this->requireResource('harbormaster-css');
$steps = mpull($steps, null, 'getPHID');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$list = new PHUIStatusListView();
foreach ($step_phids as $step_phid) {
$error = null;
if (idx($steps, $step_phid) === null) {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Missing Dependency');
$has_conflicts = true;
$error = pht(
"This dependency specifies a build step which doesn't exist.");
} else {
$bound = phutil_tag(
'strong',
array(),
idx($steps, $step_phid)->getName());
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget(pht('Build Step'))
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterStepViewController.php b/src/applications/harbormaster/controller/HarbormasterStepViewController.php
index a404b48e8..d4f159226 100644
--- a/src/applications/harbormaster/controller/HarbormasterStepViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterStepViewController.php
@@ -1,144 +1,148 @@
<?php
final class HarbormasterStepViewController
extends HarbormasterPlanController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$step = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$step) {
return new Aphront404Response();
}
$plan = $step->getBuildPlan();
$plan_id = $plan->getID();
$plan_uri = $this->getApplicationURI("plan/{$plan_id}/");
$field_list = PhabricatorCustomField::getObjectFields(
$step,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($step);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Plan %d', $plan_id), $plan_uri);
$crumbs->addTextCrumb(pht('Step %d', $id));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Build Step %d: %s', $id, $step->getName()))
->setHeaderIcon('fa-chevron-circle-right');
$properties = $this->buildPropertyList($step, $field_list);
$curtain = $this->buildCurtainView($step);
$timeline = $this->buildTransactionTimeline(
$step,
new HarbormasterBuildStepTransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$properties,
$timeline,
));
return $this->newPage()
->setTitle(pht('Step %d', $id))
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildPropertyList(
HarbormasterBuildStep $step,
PhabricatorCustomFieldList $field_list) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
try {
$implementation = $step->getStepImplementation();
} catch (Exception $ex) {
$implementation = null;
}
if ($implementation) {
$type = $implementation->getName();
} else {
$type = phutil_tag(
'em',
array(),
pht(
'Invalid Implementation ("%s")!',
$step->getClassName()));
}
$view->addProperty(pht('Step Type'), $type);
$view->addProperty(
pht('Created'),
phabricator_datetime($step->getDateCreated(), $viewer));
$field_list->appendFieldsToPropertyList(
$step,
$viewer,
$view);
$description = $step->getDescription();
if (strlen($description)) {
$view->addSectionHeader(
pht('Description'),
PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent(
new PHUIRemarkupView($viewer, $description));
}
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($view);
}
private function buildCurtainView(HarbormasterBuildStep $step) {
$viewer = $this->getViewer();
$id = $step->getID();
$curtain = $this->newCurtainView($step);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$step,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Step'))
->setHref($this->getApplicationURI("step/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit)
->setIcon('fa-pencil'));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete Step'))
->setHref($this->getApplicationURI("step/delete/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-times'));
return $curtain;
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php b/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php
index 05f06a4e9..d548ceac9 100644
--- a/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php
+++ b/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php
@@ -1,67 +1,72 @@
<?php
final class HarbormasterUnitMessageListController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needBuilds(true)
->needTargets(true)
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$unit_data = array();
if ($target_phids) {
$unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
} else {
$unit_data = array();
}
$unit = id(new HarbormasterUnitSummaryView())
+ ->setViewer($viewer)
->setBuildable($buildable)
->setUnitMessages($unit_data);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(pht('Unit Tests'));
$crumbs->setBorder(true);
$title = array(
$buildable->getMonogram(),
pht('Unit Tests'),
);
$header = id(new PHUIHeaderView())
->setHeader($buildable->getMonogram().' '.pht('Unit Tests'));
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$unit,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php b/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php
index 631c33293..5cb33c0c9 100644
--- a/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php
@@ -1,128 +1,116 @@
<?php
final class HarbormasterUnitMessageViewController
extends HarbormasterController {
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$message_id = $request->getURIData('id');
$message = id(new HarbormasterBuildUnitMessage())->load($message_id);
if (!$message) {
return new Aphront404Response();
}
$build_target = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withPHIDs(array($message->getBuildTargetPHID()))
->executeOne();
if (!$build_target) {
return new Aphront404Response();
}
$build = $build_target->getBuild();
$buildable = $build->getBuildable();
$buildable_id = $buildable->getID();
$id = $message->getID();
$display_name = $message->getUnitMessageDisplayName();
$status = $message->getResult();
$status_icon = HarbormasterUnitStatus::getUnitStatusIcon($status);
$status_color = HarbormasterUnitStatus::getUnitStatusColor($status);
$status_label = HarbormasterUnitStatus::getUnitStatusLabel($status);
$header = id(new PHUIHeaderView())
->setHeader($display_name)
->setStatus($status_icon, $status_color, $status_label);
$properties = $this->buildPropertyListView($message);
$curtain = $this->buildCurtainView($message, $build);
$unit = id(new PHUIObjectBoxView())
->setHeaderText(pht('TEST RESULT'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addPropertyList($properties);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(
pht('Unit Tests'),
"/harbormaster/unit/{$buildable_id}/");
$crumbs->addTextCrumb(pht('Unit %d', $id));
$crumbs->setBorder(true);
$title = array(
$display_name,
$buildable->getMonogram(),
);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$unit,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildPropertyListView(
HarbormasterBuildUnitMessage $message) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$view->addProperty(
pht('Run At'),
phabricator_datetime($message->getDateCreated(), $viewer));
- $details = $message->getUnitMessageDetails();
- if (strlen($details)) {
- // TODO: Use the log view here, once it gets cleaned up.
- // Shenanigans below.
- $details = phutil_tag(
- 'div',
- array(
- 'class' => 'PhabricatorMonospaced',
- 'style' =>
- 'white-space: pre-wrap; '.
- 'color: #666666; '.
- 'overflow-x: auto;',
- ),
- $details);
- } else {
- $details = phutil_tag('em', array(), pht('No details provided.'));
- }
+ $details = $message->newUnitMessageDetailsView($viewer);
$view->addSectionHeader(
pht('Details'),
PHUIPropertyListView::ICON_TESTPLAN);
$view->addTextContent($details);
return $view;
}
private function buildCurtainView(
HarbormasterBuildUnitMessage $message,
HarbormasterBuild $build) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($build);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View Build'))
->setHref($build->getURI())
->setIcon('fa-wrench'));
return $curtain;
}
}
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
index 41d31673d..25222975b 100644
--- a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
@@ -1,105 +1,108 @@
<?php
abstract class HarbormasterBuildableEngine
extends Phobject {
private $viewer;
private $actingAsPHID;
private $contentSource;
private $object;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
final public function getActingAsPHID() {
return $this->actingAsPHID;
}
final public function setContentSource(
PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
final public function getContentSource() {
return $this->contentSource;
}
final public function setObject(HarbormasterBuildableInterface $object) {
$this->object = $object;
return $this;
}
final public function getObject() {
return $this->object;
}
protected function getPublishableObject() {
return $this->getObject();
}
public function publishBuildable(
HarbormasterBuildable $old,
HarbormasterBuildable $new) {
return;
}
final public static function newForObject(
HarbormasterBuildableInterface $object,
PhabricatorUser $viewer) {
return $object->newBuildableEngine()
->setViewer($viewer)
->setObject($object);
}
final protected function newEditor() {
$publishable = $this->getPublishableObject();
$viewer = $this->getViewer();
$editor = $publishable->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$acting_as_phid = $this->getActingAsPHID();
if ($acting_as_phid !== null) {
$editor->setActingAsPHID($acting_as_phid);
}
$content_source = $this->getContentSource();
if ($content_source !== null) {
$editor->setContentSource($content_source);
}
return $editor;
}
final protected function newTransaction() {
$publishable = $this->getPublishableObject();
return $publishable->getApplicationTransactionTemplate();
}
final protected function applyTransactions(array $xactions) {
$publishable = $this->getPublishableObject();
$editor = $this->newEditor();
$editor->applyTransactions(
$publishable->getApplicationTransactionObject(),
$xactions);
}
+ public function getAuthorIdentity() {
+ return null;
+ }
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php b/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php
index c14185116..992dd4fad 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildStepQuery.php
@@ -1,98 +1,89 @@
<?php
final class HarbormasterBuildStepQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $buildPlanPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBuildPlanPHIDs(array $phids) {
$this->buildPlanPHIDs = $phids;
return $this;
}
+ public function newResultObject() {
+ return new HarbormasterBuildStep();
+ }
+
protected function loadPage() {
- $table = new HarbormasterBuildStep();
- $conn_r = $table->establishConnection('r');
-
- $data = queryfx_all(
- $conn_r,
- 'SELECT * FROM %T %Q %Q %Q',
- $table->getTableName(),
- $this->buildWhereClause($conn_r),
- $this->buildOrderClause($conn_r),
- $this->buildLimitClause($conn_r));
-
- return $table->loadAllFromArray($data);
+ return $this->loadStandardPage($this->newResultObject());
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
- if ($this->ids) {
+ if ($this->ids !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'id IN (%Ld)',
$this->ids);
}
- if ($this->phids) {
+ if ($this->phids !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'phid in (%Ls)',
$this->phids);
}
- if ($this->buildPlanPHIDs) {
+ if ($this->buildPlanPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'buildPlanPHID in (%Ls)',
$this->buildPlanPHIDs);
}
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
protected function willFilterPage(array $page) {
$plans = array();
$buildplan_phids = array_filter(mpull($page, 'getBuildPlanPHID'));
if ($buildplan_phids) {
$plans = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withPHIDs($buildplan_phids)
->setParentQuery($this)
->execute();
$plans = mpull($plans, null, 'getPHID');
}
foreach ($page as $key => $build) {
$buildable_phid = $build->getBuildPlanPHID();
if (empty($plans[$buildable_phid])) {
unset($page[$key]);
continue;
}
$build->attachBuildPlan($plans[$buildable_phid]);
}
return $page;
}
public function getQueryApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php
new file mode 100644
index 000000000..d6c2a4673
--- /dev/null
+++ b/src/applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php
@@ -0,0 +1,135 @@
+<?php
+
+final class HarbormasterAbortOlderBuildsBuildStepImplementation
+ extends HarbormasterBuildStepImplementation {
+
+ public function getName() {
+ return pht('Abort Older Builds');
+ }
+
+ public function getGenericDescription() {
+ return pht(
+ 'When building a revision, abort copies of this build plan which are '.
+ 'currently running against older diffs.');
+ }
+
+ public function getBuildStepGroupKey() {
+ return HarbormasterControlBuildStepGroup::GROUPKEY;
+ }
+
+ public function getEditInstructions() {
+ return pht(<<<EOTEXT
+When run against a revision, this build step will abort any older copies of
+the same build plan which are currently running against older diffs.
+
+There are some nuances to the behavior:
+
+ - if this build step is triggered manually, it won't abort anything;
+ - this build step won't abort manual builds;
+ - this build step won't abort anything if the diff it is building isn't
+ the active diff when it runs.
+
+Build results on outdated diffs often aren't very important, so this may
+reduce build queue load without any substantial cost.
+EOTEXT
+ );
+ }
+
+ public function willStartBuild(
+ PhabricatorUser $viewer,
+ HarbormasterBuildable $buildable,
+ HarbormasterBuild $build,
+ HarbormasterBuildPlan $plan,
+ HarbormasterBuildStep $step) {
+
+ if ($buildable->getIsManualBuildable()) {
+ // Don't abort anything if this is a manual buildable.
+ return;
+ }
+
+ $object_phid = $buildable->getBuildablePHID();
+ if (phid_get_type($object_phid) !== DifferentialDiffPHIDType::TYPECONST) {
+ // If this buildable isn't building a diff, bail out. For example, we
+ // might be building a commit. In this case, this step has no effect.
+ return;
+ }
+
+ $diff = id(new DifferentialDiffQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($object_phid))
+ ->executeOne();
+ if (!$diff) {
+ return;
+ }
+
+ $revision_id = $diff->getRevisionID();
+
+ $revision = id(new DifferentialRevisionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($revision_id))
+ ->executeOne();
+ if (!$revision) {
+ return;
+ }
+
+ $active_phid = $revision->getActiveDiffPHID();
+ if ($active_phid !== $object_phid) {
+ // If we aren't building the active diff, bail out.
+ return;
+ }
+
+ $diffs = id(new DifferentialDiffQuery())
+ ->setViewer($viewer)
+ ->withRevisionIDs(array($revision_id))
+ ->execute();
+ $abort_diff_phids = array();
+ foreach ($diffs as $diff) {
+ if ($diff->getPHID() !== $active_phid) {
+ $abort_diff_phids[] = $diff->getPHID();
+ }
+ }
+
+ if (!$abort_diff_phids) {
+ return;
+ }
+
+ // We're fetching buildables even if they have "passed" or "failed"
+ // because they may still have ongoing builds. At the time of writing
+ // only "failed" buildables may still be ongoing, but it seems likely that
+ // "passed" buildables may be ongoing in the future.
+
+ $abort_buildables = id(new HarbormasterBuildableQuery())
+ ->setViewer($viewer)
+ ->withBuildablePHIDs($abort_diff_phids)
+ ->withManualBuildables(false)
+ ->execute();
+ if (!$abort_buildables) {
+ return;
+ }
+
+ $statuses = HarbormasterBuildStatus::getIncompleteStatusConstants();
+
+ $abort_builds = id(new HarbormasterBuildQuery())
+ ->setViewer($viewer)
+ ->withBuildablePHIDs(mpull($abort_buildables, 'getPHID'))
+ ->withBuildPlanPHIDs(array($plan->getPHID()))
+ ->withBuildStatuses($statuses)
+ ->execute();
+ if (!$abort_builds) {
+ return;
+ }
+
+ foreach ($abort_builds as $abort_build) {
+ $abort_build->sendMessage(
+ $viewer,
+ HarbormasterBuildCommand::COMMAND_ABORT);
+ }
+ }
+
+ public function execute(
+ HarbormasterBuild $build,
+ HarbormasterBuildTarget $build_target) {
+ return;
+ }
+
+}
diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
index 47af99263..e0dfe05cc 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
@@ -1,327 +1,336 @@
<?php
/**
* @task autotarget Automatic Targets
*/
abstract class HarbormasterBuildStepImplementation extends Phobject {
private $settings;
private $currentWorkerTaskID;
public function setCurrentWorkerTaskID($id) {
$this->currentWorkerTaskID = $id;
return $this;
}
public function getCurrentWorkerTaskID() {
return $this->currentWorkerTaskID;
}
public static function getImplementations() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
public static function getImplementation($class) {
$base = idx(self::getImplementations(), $class);
if ($base) {
return (clone $base);
}
return null;
}
public static function requireImplementation($class) {
if (!$class) {
throw new Exception(pht('No implementation is specified!'));
}
$implementation = self::getImplementation($class);
if (!$implementation) {
throw new Exception(pht('No such implementation "%s" exists!', $class));
}
return $implementation;
}
/**
* The name of the implementation.
*/
abstract public function getName();
public function getBuildStepGroupKey() {
return HarbormasterOtherBuildStepGroup::GROUPKEY;
}
/**
* The generic description of the implementation.
*/
public function getGenericDescription() {
return '';
}
/**
* The description of the implementation, based on the current settings.
*/
public function getDescription() {
return $this->getGenericDescription();
}
public function getEditInstructions() {
return null;
}
/**
* Run the build target against the specified build.
*/
abstract public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target);
/**
* Gets the settings for this build step.
*/
public function getSettings() {
return $this->settings;
}
public function getSetting($key, $default = null) {
return idx($this->settings, $key, $default);
}
/**
* Loads the settings for this build step implementation from a build
* step or target.
*/
final public function loadSettings($build_object) {
$this->settings = $build_object->getDetails();
return $this;
}
/**
* Return the name of artifacts produced by this command.
*
* Future steps will calculate all available artifact mappings
* before them and filter on the type.
*
* @return array The mappings of artifact names to their types.
*/
public function getArtifactInputs() {
return array();
}
public function getArtifactOutputs() {
return array();
}
public function getDependencies(HarbormasterBuildStep $build_step) {
$dependencies = $build_step->getDetail('dependsOn', array());
$inputs = $build_step->getStepImplementation()->getArtifactInputs();
$inputs = ipull($inputs, null, 'key');
$artifacts = $this->getAvailableArtifacts(
$build_step->getBuildPlan(),
$build_step,
null);
foreach ($artifacts as $key => $type) {
if (!array_key_exists($key, $inputs)) {
unset($artifacts[$key]);
}
}
$artifact_steps = ipull($artifacts, 'step');
$artifact_steps = mpull($artifact_steps, 'getPHID');
$dependencies = array_merge($dependencies, $artifact_steps);
return $dependencies;
}
/**
* Returns a list of all artifacts made available in the build plan.
*/
public static function getAvailableArtifacts(
HarbormasterBuildPlan $build_plan,
$current_build_step,
$artifact_type) {
$steps = id(new HarbormasterBuildStepQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildPlanPHIDs(array($build_plan->getPHID()))
->execute();
$artifacts = array();
$artifact_arrays = array();
foreach ($steps as $step) {
if ($current_build_step !== null &&
$step->getPHID() === $current_build_step->getPHID()) {
continue;
}
$implementation = $step->getStepImplementation();
$array = $implementation->getArtifactOutputs();
$array = ipull($array, 'type', 'key');
foreach ($array as $name => $type) {
if ($type !== $artifact_type && $artifact_type !== null) {
continue;
}
$artifacts[$name] = array('type' => $type, 'step' => $step);
}
}
return $artifacts;
}
/**
* Convert a user-provided string with variables in it, like:
*
* ls ${dirname}
*
* ...into a string with variables merged into it safely:
*
* ls 'dir with spaces'
*
* @param string Name of a `vxsprintf` function, like @{function:vcsprintf}.
* @param string User-provided pattern string containing `${variables}`.
* @param dict List of available replacement variables.
* @return string String with variables replaced safely into it.
*/
protected function mergeVariables($function, $pattern, array $variables) {
- $regexp = '@\\$\\{(?P<name>[a-z\\./-]+)\\}@';
+ $regexp = '@\\$\\{(?P<name>[a-z\\./_-]+)\\}@';
$matches = null;
preg_match_all($regexp, $pattern, $matches);
$argv = array();
foreach ($matches['name'] as $name) {
if (!array_key_exists($name, $variables)) {
throw new Exception(pht("No such variable '%s'!", $name));
}
$argv[] = $variables[$name];
}
$pattern = str_replace('%', '%%', $pattern);
$pattern = preg_replace($regexp, '%s', $pattern);
return call_user_func($function, $pattern, $argv);
}
public function getFieldSpecifications() {
return array();
}
protected function formatSettingForDescription($key, $default = null) {
return $this->formatValueForDescription($this->getSetting($key, $default));
}
protected function formatValueForDescription($value) {
if (strlen($value)) {
return phutil_tag('strong', array(), $value);
} else {
return phutil_tag('em', array(), pht('(null)'));
}
}
public function supportsWaitForMessage() {
return false;
}
public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
if (!$this->supportsWaitForMessage()) {
return false;
}
$wait = $target->getDetail('builtin.wait-for-message');
return ($wait == 'wait');
}
protected function shouldAbort(
HarbormasterBuild $build,
HarbormasterBuildTarget $target) {
return $build->getBuildGeneration() !== $target->getBuildGeneration();
}
protected function resolveFutures(
HarbormasterBuild $build,
HarbormasterBuildTarget $target,
array $futures) {
$futures = new FutureIterator($futures);
foreach ($futures->setUpdateInterval(5) as $key => $future) {
if ($future === null) {
$build->reload();
if ($this->shouldAbort($build, $target)) {
throw new HarbormasterBuildAbortedException();
}
}
}
}
protected function logHTTPResponse(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target,
BaseHTTPFuture $future,
$label) {
list($status, $body, $headers) = $future->resolve();
$header_lines = array();
// TODO: We don't currently preserve the entire "HTTP" response header, but
// should. Once we do, reproduce it here faithfully.
$status_code = $status->getStatusCode();
$header_lines[] = "HTTP {$status_code}";
foreach ($headers as $header) {
list($head, $tail) = $header;
$header_lines[] = "{$head}: {$tail}";
}
$header_lines = implode("\n", $header_lines);
$build_target
->newLog($label, 'http.head')
->append($header_lines);
$build_target
->newLog($label, 'http.body')
->append($body);
}
protected function logSilencedCall(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target,
$label) {
$build_target
->newLog($label, 'silenced')
->append(
pht(
'Declining to make service call because `phabricator.silent` is '.
'enabled in configuration.'));
}
+ public function willStartBuild(
+ PhabricatorUser $viewer,
+ HarbormasterBuildable $buildable,
+ HarbormasterBuild $build,
+ HarbormasterBuildPlan $plan,
+ HarbormasterBuildStep $step) {
+ return;
+ }
+
/* -( Automatic Targets )-------------------------------------------------- */
public function getBuildStepAutotargetStepKey() {
return null;
}
public function getBuildStepAutotargetPlanKey() {
throw new PhutilMethodNotImplementedException();
}
public function shouldRequireAutotargeting() {
return false;
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
index 89d4002ee..ef4cc0daf 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
@@ -1,215 +1,237 @@
<?php
final class HarbormasterBuildkiteBuildStepImplementation
extends HarbormasterBuildStepImplementation {
public function getName() {
return pht('Build with Buildkite');
}
public function getGenericDescription() {
return pht('Trigger a build in Buildkite.');
}
public function getBuildStepGroupKey() {
return HarbormasterExternalBuildStepGroup::GROUPKEY;
}
public function getDescription() {
return pht('Run a build in Buildkite.');
}
public function getEditInstructions() {
$hook_uri = '/harbormaster/hook/buildkite/';
$hook_uri = PhabricatorEnv::getProductionURI($hook_uri);
return pht(<<<EOTEXT
WARNING: This build step is new and experimental!
To build **revisions** with Buildkite, they must:
- belong to a tracked repository;
- the repository must have a Staging Area configured;
- you must configure a Buildkite pipeline for that Staging Area; and
- you must configure the webhook described below.
To build **commits** with Buildkite, they must:
- belong to a tracked repository;
- you must configure a Buildkite pipeline for that repository; and
- you must configure the webhook described below.
Webhook Configuration
=====================
In {nav Settings} for your Organization in Buildkite, under
{nav Notification Services}, add a new **Webook Notification**.
Use these settings:
- **Webhook URL**: %s
- **Token**: The "Webhook Token" field below and the "Token" field in
Buildkite should both be set to the same nonempty value (any random
secret). You can use copy/paste the value Buildkite generates into
this form.
- **Events**: Only **build.finish** needs to be active.
Environment
===========
These variables will be available in the build environment:
| Variable | Description |
|----------|-------------|
| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target.
EOTEXT
,
$hook_uri);
}
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
$viewer = PhabricatorUser::getOmnipotentUser();
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$this->logSilencedCall($build, $build_target, pht('Buildkite'));
throw new HarbormasterBuildFailureException();
}
$buildable = $build->getBuildable();
$object = $buildable->getBuildableObject();
if (!($object instanceof HarbormasterBuildkiteBuildableInterface)) {
throw new Exception(
pht('This object does not support builds with Buildkite.'));
}
$organization = $this->getSetting('organization');
$pipeline = $this->getSetting('pipeline');
$uri = urisprintf(
'https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds',
$organization,
$pipeline);
$data_structure = array(
'commit' => $object->getBuildkiteCommit(),
'branch' => $object->getBuildkiteBranch(),
'message' => pht(
'Harbormaster Build %s ("%s") for %s',
$build->getID(),
$build->getName(),
$buildable->getMonogram()),
'env' => array(
'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),
),
'meta_data' => array(
'buildTargetPHID' => $build_target->getPHID(),
+
+ // See PHI611. These are undocumented secret magic.
+ 'phabricator:build:id' => (int)$build->getID(),
+ 'phabricator:build:url' =>
+ PhabricatorEnv::getProductionURI($build->getURI()),
+ 'phabricator:buildable:id' => (int)$buildable->getID(),
+ 'phabricator:buildable:url' =>
+ PhabricatorEnv::getProductionURI($buildable->getURI()),
),
);
+ $engine = HarbormasterBuildableEngine::newForObject(
+ $object,
+ $viewer);
+
+ $author_identity = $engine->getAuthorIdentity();
+ if ($author_identity) {
+ $data_structure += array(
+ 'author' => array(
+ 'name' => $author_identity->getIdentityDisplayName(),
+ 'email' => $author_identity->getIdentityEmailAddress(),
+ ),
+ );
+ }
+
$json_data = phutil_json_encode($data_structure);
$credential_phid = $this->getSetting('token');
$api_token = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withPHIDs(array($credential_phid))
->needSecrets(true)
->executeOne();
if (!$api_token) {
throw new Exception(
pht(
'Unable to load API token ("%s")!',
$credential_phid));
}
$token = $api_token->getSecret()->openEnvelope();
$future = id(new HTTPSFuture($uri, $json_data))
->setMethod('POST')
->addHeader('Content-Type', 'application/json')
->addHeader('Accept', 'application/json')
->addHeader('Authorization', "Bearer {$token}")
->setTimeout(60);
$this->resolveFutures(
$build,
$build_target,
array($future));
$this->logHTTPResponse($build, $build_target, $future, pht('Buildkite'));
list($status, $body) = $future->resolve();
if ($status->isError()) {
throw new HarbormasterBuildFailureException();
}
$response = phutil_json_decode($body);
$uri_key = 'web_url';
$build_uri = idx($response, $uri_key);
if (!$build_uri) {
throw new Exception(
pht(
'Buildkite did not return a "%s"!',
$uri_key));
}
$target_phid = $build_target->getPHID();
$api_method = 'harbormaster.createartifact';
$api_params = array(
'buildTargetPHID' => $target_phid,
'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,
'artifactKey' => 'buildkite.uri',
'artifactData' => array(
'uri' => $build_uri,
'name' => pht('View in Buildkite'),
'ui.external' => true,
),
);
id(new ConduitCall($api_method, $api_params))
->setUser($viewer)
->execute();
}
public function getFieldSpecifications() {
return array(
'token' => array(
'name' => pht('API Token'),
'type' => 'credential',
'credential.type'
=> PassphraseTokenCredentialType::CREDENTIAL_TYPE,
'credential.provides'
=> PassphraseTokenCredentialType::PROVIDES_TYPE,
'required' => true,
),
'organization' => array(
'name' => pht('Organization Name'),
'type' => 'text',
'required' => true,
),
'pipeline' => array(
'name' => pht('Pipeline Name'),
'type' => 'text',
'required' => true,
),
'webhook.token' => array(
'name' => pht('Webhook Token'),
'type' => 'text',
'required' => true,
),
);
}
public function supportsWaitForMessage() {
return false;
}
public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
return true;
}
}
diff --git a/src/applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php b/src/applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php
new file mode 100644
index 000000000..f49fd0b12
--- /dev/null
+++ b/src/applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php
@@ -0,0 +1,20 @@
+<?php
+
+final class HarbormasterControlBuildStepGroup
+ extends HarbormasterBuildStepGroup {
+
+ const GROUPKEY = 'harbormaster.control';
+
+ public function getGroupName() {
+ return pht('Flow Control');
+ }
+
+ public function getGroupOrder() {
+ return 5000;
+ }
+
+ public function shouldShowIfEmpty() {
+ return false;
+ }
+
+}
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php
index 7d4f2e7c8..b24c4805e 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildable.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php
@@ -1,377 +1,386 @@
<?php
final class HarbormasterBuildable
extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
HarbormasterBuildableInterface,
PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $containerPHID;
protected $buildableStatus;
protected $isManualBuildable;
private $buildableObject = self::ATTACHABLE;
private $containerObject = self::ATTACHABLE;
private $builds = self::ATTACHABLE;
public static function initializeNewBuildable(PhabricatorUser $actor) {
return id(new HarbormasterBuildable())
->setIsManualBuildable(0)
->setBuildableStatus(HarbormasterBuildableStatus::STATUS_PREPARING);
}
public function getMonogram() {
return 'B'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
/**
* Returns an existing buildable for the object's PHID or creates a
* new buildable implicitly if needed.
*/
public static function createOrLoadExisting(
PhabricatorUser $actor,
$buildable_object_phid,
$container_object_phid) {
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($actor)
->withBuildablePHIDs(array($buildable_object_phid))
->withManualBuildables(false)
->setLimit(1)
->executeOne();
if ($buildable) {
return $buildable;
}
$buildable = self::initializeNewBuildable($actor)
->setBuildablePHID($buildable_object_phid)
->setContainerPHID($container_object_phid);
$buildable->save();
return $buildable;
}
/**
* Start builds for a given buildable.
*
* @param phid PHID of the object to build.
* @param phid Container PHID for the buildable.
* @param list<HarbormasterBuildRequest> List of builds to perform.
* @return void
*/
public static function applyBuildPlans(
$phid,
$container_phid,
array $requests) {
assert_instances_of($requests, 'HarbormasterBuildRequest');
if (!$requests) {
return;
}
// Skip all of this logic if the Harbormaster application
// isn't currently installed.
$harbormaster_app = 'PhabricatorHarbormasterApplication';
if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) {
return;
}
$viewer = PhabricatorUser::getOmnipotentUser();
$buildable = self::createOrLoadExisting(
$viewer,
$phid,
$container_phid);
$plan_phids = mpull($requests, 'getBuildPlanPHID');
$plans = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withPHIDs($plan_phids)
->execute();
$plans = mpull($plans, null, 'getPHID');
foreach ($requests as $request) {
$plan_phid = $request->getBuildPlanPHID();
$plan = idx($plans, $plan_phid);
if (!$plan) {
throw new Exception(
pht(
'Failed to load build plan ("%s").',
$plan_phid));
}
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;
}
$parameters = $request->getBuildParameters();
$buildable->applyPlan($plan, $parameters, $request->getInitiatorPHID());
}
}
public function applyPlan(
HarbormasterBuildPlan $plan,
array $parameters,
$initiator_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$build = HarbormasterBuild::initializeNewBuild($viewer)
->setBuildablePHID($this->getPHID())
->setBuildPlanPHID($plan->getPHID())
->setBuildParameters($parameters)
->setBuildStatus(HarbormasterBuildStatus::STATUS_PENDING);
if ($initiator_phid) {
$build->setInitiatorPHID($initiator_phid);
}
$auto_key = $plan->getPlanAutoKey();
if ($auto_key) {
$build->setPlanAutoKey($auto_key);
}
$build->save();
+ $steps = id(new HarbormasterBuildStepQuery())
+ ->setViewer($viewer)
+ ->withBuildPlanPHIDs(array($plan->getPHID()))
+ ->execute();
+
+ foreach ($steps as $step) {
+ $step->willStartBuild($viewer, $this, $build, $plan);
+ }
+
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildID' => $build->getID(),
),
array(
'objectPHID' => $build->getPHID(),
));
return $build;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'containerPHID' => 'phid?',
'buildableStatus' => 'text32',
'isManualBuildable' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_container' => array(
'columns' => array('containerPHID'),
),
'key_manual' => array(
'columns' => array('isManualBuildable'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildablePHIDType::TYPECONST);
}
public function attachBuildableObject($buildable_object) {
$this->buildableObject = $buildable_object;
return $this;
}
public function getBuildableObject() {
return $this->assertAttached($this->buildableObject);
}
public function attachContainerObject($container_object) {
$this->containerObject = $container_object;
return $this;
}
public function getContainerObject() {
return $this->assertAttached($this->containerObject);
}
public function attachBuilds(array $builds) {
assert_instances_of($builds, 'HarbormasterBuild');
$this->builds = $builds;
return $this;
}
public function getBuilds() {
return $this->assertAttached($this->builds);
}
/* -( Status )------------------------------------------------------------- */
public function getBuildableStatusObject() {
$status = $this->getBuildableStatus();
return HarbormasterBuildableStatus::newBuildableStatusObject($status);
}
public function getStatusIcon() {
return $this->getBuildableStatusObject()->getIcon();
}
public function getStatusDisplayName() {
return $this->getBuildableStatusObject()->getDisplayName();
}
public function getStatusColor() {
return $this->getBuildableStatusObject()->getColor();
}
public function isPreparing() {
return $this->getBuildableStatusObject()->isPreparing();
}
public function isBuilding() {
return $this->getBuildableStatusObject()->isBuilding();
}
/* -( Messages )----------------------------------------------------------- */
public function sendMessage(
PhabricatorUser $viewer,
$message_type,
$queue_update) {
$message = HarbormasterBuildMessage::initializeNewMessage($viewer)
->setReceiverPHID($this->getPHID())
->setType($message_type)
->save();
if ($queue_update) {
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildablePHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
));
}
return $message;
}
/* -( 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 getHarbormasterBuildableDisplayPHID() {
return $this->getBuildableObject()->getHarbormasterBuildableDisplayPHID();
}
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();
}
public function newBuildableEngine() {
return $this->getBuildableObject()->newBuildableEngine();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($this->getPHID()))
->execute();
foreach ($builds as $build) {
$engine->destroyObject($build);
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($this->getPHID()))
->execute();
foreach ($messages as $message) {
$engine->destroyObject($message);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
index ae0f6b13f..6acbac646 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,488 +1,517 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorConduitResultInterface,
PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $buildGeneration;
protected $buildParameters = array();
protected $initiatorPHID;
protected $planAutoKey;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
private $buildTargets = self::ATTACHABLE;
private $unprocessedCommands = self::ATTACHABLE;
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE)
->setBuildGeneration(0);
}
public function delete() {
$this->openTransaction();
$this->deleteUnprocessedCommands();
$result = parent::delete();
$this->saveTransaction();
return $result;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'buildParameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'buildStatus' => 'text32',
'buildGeneration' => 'uint32',
'planAutoKey' => 'text32?',
'initiatorPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_status' => array(
'columns' => array('buildStatus'),
),
'key_planautokey' => array(
'columns' => array('buildablePHID', 'planAutoKey'),
'unique' => true,
),
'key_initiator' => array(
'columns' => array('initiatorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPHIDType::TYPECONST);
}
public function attachBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getName() {
if ($this->getBuildPlan()) {
return $this->getBuildPlan()->getName();
}
return pht('Build');
}
public function attachBuildPlan(
HarbormasterBuildPlan $build_plan = null) {
$this->buildPlan = $build_plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getBuildTargets() {
return $this->assertAttached($this->buildTargets);
}
public function attachBuildTargets(array $targets) {
$this->buildTargets = $targets;
return $this;
}
public function isBuilding() {
return $this->getBuildStatusObject()->isBuilding();
}
public function isAutobuild() {
return ($this->getPlanAutoKey() !== null);
}
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
'buildable.revision' => null,
'buildable.commit' => null,
'repository.callsign' => null,
'repository.phid' => null,
'repository.vcs' => null,
'repository.uri' => null,
'step.timestamp' => null,
'build.id' => null,
'initiator.phid' => null,
);
foreach ($this->getBuildParameters() as $key => $value) {
$results['build/'.$key] = $value;
}
$buildable = $this->getBuildable();
$object = $buildable->getBuildableObject();
$object_variables = $object->getBuildVariables();
$results = $object_variables + $results;
$results['step.timestamp'] = time();
$results['build.id'] = $this->getID();
$results['initiator.phid'] = $this->getInitiatorPHID();
return $results;
}
public static function getAvailableBuildVariables() {
$objects = id(new PhutilClassMapQuery())
->setAncestorClass('HarbormasterBuildableInterface')
->execute();
$variables = array();
$variables[] = array(
'step.timestamp' => pht('The current UNIX timestamp.'),
'build.id' => pht('The ID of the current build.'),
'target.phid' => pht('The PHID of the current build target.'),
'initiator.phid' => pht(
'The PHID of the user or Object that initiated the build, '.
'if applicable.'),
);
foreach ($objects as $object) {
$variables[] = $object->getAvailableBuildVariables();
}
$variables = array_mergev($variables);
return $variables;
}
public function isComplete() {
return $this->getBuildStatusObject()->isComplete();
}
public function isPaused() {
return $this->getBuildStatusObject()->isPaused();
}
public function isPassed() {
return $this->getBuildStatusObject()->isPassed();
}
public function getURI() {
$id = $this->getID();
return "/harbormaster/build/{$id}/";
}
protected function getBuildStatusObject() {
$status_key = $this->getBuildStatus();
return HarbormasterBuildStatus::newBuildStatusObject($status_key);
}
/* -( Build Commands )----------------------------------------------------- */
private function getUnprocessedCommands() {
return $this->assertAttached($this->unprocessedCommands);
}
public function attachUnprocessedCommands(array $commands) {
$this->unprocessedCommands = $commands;
return $this;
}
public function canRestartBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isRestarting();
}
public function canPauseBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete() &&
!$this->isPaused() &&
!$this->isPausing();
}
public function canAbortBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete();
}
public function canResumeBuild() {
if ($this->isAutobuild()) {
return false;
}
return $this->isPaused() &&
!$this->isResuming();
}
public function isPausing() {
$is_pausing = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_pausing = true;
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_pausing = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_pausing = true;
break;
}
}
return $is_pausing;
}
public function isResuming() {
$is_resuming = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_resuming = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_resuming = false;
break;
}
}
return $is_resuming;
}
public function isRestarting() {
$is_restarting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
break;
}
}
return $is_restarting;
}
public function isAborting() {
$is_aborting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_aborting = true;
break;
}
}
return $is_aborting;
}
public function deleteUnprocessedCommands() {
foreach ($this->getUnprocessedCommands() as $key => $command_object) {
$command_object->delete();
unset($this->unprocessedCommands[$key]);
}
return $this;
}
public function canIssueCommand(PhabricatorUser $viewer, $command) {
try {
$this->assertCanIssueCommand($viewer, $command);
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanIssueCommand(PhabricatorUser $viewer, $command) {
$need_edit = false;
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_ABORT:
$need_edit = true;
break;
default:
throw new Exception(
pht(
'Invalid Harbormaster build command "%s".',
$command));
}
// Issuing these commands requires that you be able to edit the build, to
// prevent enemy engineers from sabotaging your builds. See T9614.
if ($need_edit) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$this->getBuildPlan(),
PhabricatorPolicyCapability::CAN_EDIT);
}
}
+ public function sendMessage(PhabricatorUser $viewer, $command) {
+ // TODO: This should not be an editor transaction, but there are plans to
+ // merge BuildCommand into BuildMessage which should moot this. As this
+ // exists today, it can race against BuildEngine.
+
+ // This is a bogus content source, but this whole flow should be obsolete
+ // soon.
+ $content_source = PhabricatorContentSource::newForSource(
+ PhabricatorConsoleContentSource::SOURCECONST);
+
+ $editor = id(new HarbormasterBuildTransactionEditor())
+ ->setActor($viewer)
+ ->setContentSource($content_source)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true);
+
+ $viewer_phid = $viewer->getPHID();
+ if (!$viewer_phid) {
+ $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
+ $editor->setActingAsPHID($acting_phid);
+ }
+
+ $xaction = id(new HarbormasterBuildTransaction())
+ ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
+ ->setNewValue($command);
+
+ $editor->applyTransactions($this, array($xaction));
+ }
+
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildTransactionEditor();
}
public function 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.');
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildablePHID')
->setType('phid')
->setDescription(pht('PHID of the object this build is building.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildPlanPHID')
->setType('phid')
->setDescription(pht('PHID of the build plan being run.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildStatus')
->setType('map<string, wild>')
->setDescription(pht('The current status of this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('initiatorPHID')
->setType('phid')
->setDescription(pht('The person (or thing) that started this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this build.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getBuildStatus();
return array(
'buildablePHID' => $this->getBuildablePHID(),
'buildPlanPHID' => $this->getBuildPlanPHID(),
'buildStatus' => array(
'value' => $status,
'name' => HarbormasterBuildStatus::getBuildStatusName($status),
'color.ansi' =>
HarbormasterBuildStatus::getBuildStatusANSIColor($status),
),
'initiatorPHID' => nonempty($this->getInitiatorPHID(), null),
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new HarbormasterQueryBuildsSearchEngineAttachment())
->setAttachmentKey('querybuilds'),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withBuildPHIDs(array($this->getPHID()))
->execute();
foreach ($targets as $target) {
$engine->destroyObject($target);
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($this->getPHID()))
->execute();
foreach ($messages as $message) {
$engine->destroyObject($message);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
index 5bc358ccc..2b160445d 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
@@ -1,780 +1,786 @@
<?php
final class HarbormasterBuildLog
extends HarbormasterDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $buildTargetPHID;
protected $logSource;
protected $logType;
protected $duration;
protected $live;
protected $filePHID;
protected $byteLength;
protected $chunkFormat;
protected $lineMap = array();
private $buildTarget = self::ATTACHABLE;
private $rope;
private $isOpen;
private $lock;
const CHUNK_BYTE_LIMIT = 1048576;
public function __construct() {
$this->rope = new PhutilRope();
}
public function __destruct() {
if ($this->isOpen) {
$this->closeBuildLog();
}
if ($this->lock) {
if ($this->lock->isLocked()) {
$this->lock->unlock();
}
}
}
public static function initializeNewBuildLog(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildLog())
->setBuildTargetPHID($build_target->getPHID())
->setDuration(null)
->setLive(1)
->setByteLength(0)
->setChunkFormat(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
public function scheduleRebuild($force) {
PhabricatorWorker::scheduleTask(
'HarbormasterLogWorker',
array(
'logPHID' => $this->getPHID(),
'force' => $force,
),
array(
'objectPHID' => $this->getPHID(),
));
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'lineMap' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
// T6203/NULLABILITY
// It seems like these should be non-nullable? All logs should have a
// source, etc.
'logSource' => 'text255?',
'logType' => 'text255?',
'duration' => 'uint32?',
'live' => 'bool',
'filePHID' => 'phid?',
'byteLength' => 'uint64',
'chunkFormat' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildtarget' => array(
'columns' => array('buildTargetPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildLogPHIDType::TYPECONST);
}
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
}
public function getBuildTarget() {
return $this->assertAttached($this->buildTarget);
}
public function getName() {
return pht('Build Log');
}
public function newChunkIterator() {
return id(new HarbormasterBuildLogChunkIterator($this))
->setPageSize(8);
}
public function newDataIterator() {
return $this->newChunkIterator()
->setAsString(true);
}
private function loadLastChunkInfo() {
$chunk_table = new HarbormasterBuildLogChunk();
$conn_w = $chunk_table->establishConnection('w');
return queryfx_one(
$conn_w,
'SELECT id, size, encoding FROM %T WHERE logID = %d
ORDER BY id DESC LIMIT 1',
$chunk_table->getTableName(),
$this->getID());
}
public function loadData($offset, $length) {
$end = ($offset + $length);
$chunks = id(new HarbormasterBuildLogChunk())->loadAllWhere(
'logID = %d AND headOffset < %d AND tailOffset >= %d
ORDER BY headOffset ASC',
$this->getID(),
$end,
$offset);
// Make sure that whatever we read out of the database is a single
// contiguous range which contains all of the requested bytes.
$ranges = array();
foreach ($chunks as $chunk) {
$ranges[] = array(
'head' => $chunk->getHeadOffset(),
'tail' => $chunk->getTailOffset(),
);
}
$ranges = isort($ranges, 'head');
$ranges = array_values($ranges);
$count = count($ranges);
for ($ii = 0; $ii < ($count - 1); $ii++) {
if ($ranges[$ii + 1]['head'] === $ranges[$ii]['tail']) {
$ranges[$ii + 1]['head'] = $ranges[$ii]['head'];
unset($ranges[$ii]);
}
}
if (count($ranges) !== 1) {
$display_ranges = array();
foreach ($ranges as $range) {
$display_ranges[] = pht(
'(%d - %d)',
$range['head'],
$range['tail']);
}
if (!$display_ranges) {
$display_ranges[] = pht('<null>');
}
throw new Exception(
pht(
'Attempt to load log bytes (%d - %d) failed: failed to '.
'load a single contiguous range. Actual ranges: %s.',
implode('; ', $display_ranges)));
}
$range = head($ranges);
if ($range['head'] > $offset || $range['tail'] < $end) {
throw new Exception(
pht(
'Attempt to load log bytes (%d - %d) failed: the loaded range '.
'(%d - %d) does not span the requested range.',
$offset,
$end,
$range['head'],
$range['tail']));
}
$parts = array();
foreach ($chunks as $chunk) {
$parts[] = $chunk->getChunkDisplayText();
}
$parts = implode('', $parts);
$chop_head = ($offset - $range['head']);
$chop_tail = ($range['tail'] - $end);
if ($chop_head) {
$parts = substr($parts, $chop_head);
}
if ($chop_tail) {
$parts = substr($parts, 0, -$chop_tail);
}
return $parts;
}
public function getLineSpanningRange($min_line, $max_line) {
$map = $this->getLineMap();
if (!$map) {
throw new Exception(pht('No line map.'));
}
$min_pos = 0;
$min_line = 0;
$max_pos = $this->getByteLength();
list($map) = $map;
foreach ($map as $marker) {
list($offset, $count) = $marker;
if ($count < $min_line) {
if ($offset > $min_pos) {
$min_pos = $offset;
$min_line = $count;
}
}
if ($count > $max_line) {
$max_pos = min($max_pos, $offset);
break;
}
}
return array($min_pos, $max_pos, $min_line);
}
public function getReadPosition($read_offset) {
$position = array(0, 0);
$map = $this->getLineMap();
if (!$map) {
throw new Exception(pht('No line map.'));
}
list($map) = $map;
foreach ($map as $marker) {
list($offset, $count) = $marker;
if ($offset > $read_offset) {
break;
}
$position = $marker;
}
return $position;
}
public function getLogText() {
// TODO: Remove this method since it won't scale for big logs.
$all_chunks = $this->newChunkIterator();
$full_text = array();
foreach ($all_chunks as $chunk) {
$full_text[] = $chunk->getChunkDisplayText();
}
return implode('', $full_text);
}
public function getURI() {
$id = $this->getID();
return "/harbormaster/log/view/{$id}/";
}
public function getRenderURI($lines) {
if (strlen($lines)) {
$lines = '$'.$lines;
}
$id = $this->getID();
return "/harbormaster/log/render/{$id}/{$lines}";
}
/* -( Chunks )------------------------------------------------------------- */
public function canCompressLog() {
return function_exists('gzdeflate');
}
public function compressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP);
}
public function decompressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
private function processLog($mode) {
if (!$this->getLock()->isLocked()) {
throw new Exception(
pht(
'You can not process build log chunks unless the log lock is '.
'held.'));
}
$chunks = $this->newChunkIterator();
// NOTE: Because we're going to insert new chunks, we need to stop the
// iterator once it hits the final chunk which currently exists. Otherwise,
// it may start consuming chunks we just wrote and run forever.
$last = $this->loadLastChunkInfo();
if ($last) {
$chunks->setRange(null, $last['id']);
}
$byte_limit = self::CHUNK_BYTE_LIMIT;
$rope = new PhutilRope();
$this->openTransaction();
$offset = 0;
foreach ($chunks as $chunk) {
$rope->append($chunk->getChunkDisplayText());
$chunk->delete();
while ($rope->getByteLength() > $byte_limit) {
$offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
}
while ($rope->getByteLength()) {
$offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
$this
->setChunkFormat($mode)
->save();
$this->saveTransaction();
}
private function writeEncodedChunk(
PhutilRope $rope,
$offset,
$length,
$mode) {
$data = $rope->getPrefixBytes($length);
$size = strlen($data);
switch ($mode) {
case HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT:
// Do nothing.
break;
case HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP:
$data = gzdeflate($data);
if ($data === false) {
throw new Exception(pht('Failed to gzdeflate() log data!'));
}
break;
default:
throw new Exception(pht('Unknown chunk encoding "%s"!', $mode));
}
$this->writeChunk($mode, $offset, $size, $data);
$rope->removeBytesFromHead($size);
return $size;
}
private function writeChunk($encoding, $offset, $raw_size, $data) {
$head_offset = $offset;
$tail_offset = $offset + $raw_size;
return id(new HarbormasterBuildLogChunk())
->setLogID($this->getID())
->setEncoding($encoding)
->setHeadOffset($head_offset)
->setTailOffset($tail_offset)
->setSize($raw_size)
->setChunk($data)
->save();
}
/* -( Writing )------------------------------------------------------------ */
public function getLock() {
if (!$this->lock) {
$phid = $this->getPHID();
$phid_key = PhabricatorHash::digestToLength($phid, 14);
$lock_key = "build.log({$phid_key})";
$lock = PhabricatorGlobalLock::newLock($lock_key);
$this->lock = $lock;
}
return $this->lock;
}
public function openBuildLog() {
if ($this->isOpen) {
throw new Exception(pht('This build log is already open!'));
}
$is_new = !$this->getID();
if ($is_new) {
$this->save();
}
$this->getLock()->lock();
$this->isOpen = true;
$this->reload();
if (!$this->getLive()) {
$this->setLive(1)->save();
}
return $this;
}
public function closeBuildLog($forever = true) {
if (!$this->isOpen) {
throw new Exception(
pht(
'You must openBuildLog() before you can closeBuildLog().'));
}
$this->flush();
if ($forever) {
$start = $this->getDateCreated();
$now = PhabricatorTime::getNow();
$this
->setDuration($now - $start)
->setLive(0)
->save();
}
$this->getLock()->unlock();
$this->isOpen = false;
if ($forever) {
$this->scheduleRebuild(false);
}
return $this;
}
public function append($content) {
if (!$this->isOpen) {
throw new Exception(
pht(
'You must openBuildLog() before you can append() content to '.
'the log.'));
}
$content = (string)$content;
$this->rope->append($content);
$this->flush();
return $this;
}
private function flush() {
// TODO: Maybe don't flush more than a couple of times per second. If a
// caller writes a single character over and over again, we'll currently
// spend a lot of time flushing that.
$chunk_table = id(new HarbormasterBuildLogChunk())->getTableName();
$chunk_limit = self::CHUNK_BYTE_LIMIT;
$encoding_text = HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT;
$rope = $this->rope;
while (true) {
$length = $rope->getByteLength();
if (!$length) {
break;
}
$conn_w = $this->establishConnection('w');
$last = $this->loadLastChunkInfo();
$can_append =
($last) &&
($last['encoding'] == $encoding_text) &&
($last['size'] < $chunk_limit);
if ($can_append) {
$append_id = $last['id'];
$prefix_size = $last['size'];
} else {
$append_id = null;
$prefix_size = 0;
}
$data_limit = ($chunk_limit - $prefix_size);
$append_data = $rope->getPrefixBytes($data_limit);
$data_size = strlen($append_data);
$this->openTransaction();
if ($append_id) {
queryfx(
$conn_w,
'UPDATE %T SET
chunk = CONCAT(chunk, %B),
size = %d,
tailOffset = headOffset + %d
WHERE
id = %d',
$chunk_table,
$append_data,
$prefix_size + $data_size,
$prefix_size + $data_size,
$append_id);
} else {
$this->writeChunk(
$encoding_text,
$this->getByteLength(),
$data_size,
$append_data);
}
$this->updateLineMap($append_data);
$this->save();
$this->saveTransaction();
$rope->removeBytesFromHead($data_size);
}
}
public function updateLineMap($append_data, $marker_distance = null) {
$this->byteLength += strlen($append_data);
if (!$marker_distance) {
$marker_distance = (self::CHUNK_BYTE_LIMIT / 2);
}
if (!$this->lineMap) {
$this->lineMap = array(
array(),
0,
0,
null,
);
}
list($map, $map_bytes, $line_count, $prefix) = $this->lineMap;
$buffer = $append_data;
if ($prefix) {
$prefix = base64_decode($prefix);
$buffer = $prefix.$buffer;
}
if ($map) {
list($last_marker, $last_count) = last($map);
} else {
$last_marker = 0;
$last_count = 0;
}
$max_utf8_width = 8;
$next_marker = $last_marker + $marker_distance;
$pos = 0;
$len = strlen($buffer);
while (true) {
// If we only have a few bytes left in the buffer, leave it as a prefix
// for next time.
if (($len - $pos) <= ($max_utf8_width * 2)) {
$prefix = substr($buffer, $pos);
break;
}
// The next slice we're going to look at is the smaller of:
//
// - the number of bytes we need to make it to the next marker; or
// - all the bytes we have left, minus one.
$slice_length = min(
($marker_distance - $map_bytes),
($len - $pos) - 1);
// We don't slice all the way to the end for two reasons.
// First, we want to avoid slicing immediately after a "\r" if we don't
// know what the next character is, because we want to make sure to
// count "\r\n" as a single newline, rather than counting the "\r" as
// a newline and then later counting the "\n" as another newline.
// Second, we don't want to slice in the middle of a UTF8 character if
// we can help it. We may not be able to avoid this, since the whole
// buffer may just be binary data, but in most cases we can backtrack
// a little bit and try to make it out of emoji or other legitimate
// multibyte UTF8 characters which appear in the log.
$min_width = max(1, $slice_length - $max_utf8_width);
while ($slice_length >= $min_width) {
$here = $buffer[$pos + ($slice_length - 1)];
$next = $buffer[$pos + ($slice_length - 1) + 1];
// If this is "\r" and the next character is "\n", extend the slice
// to include the "\n". Otherwise, we're fine to slice here since we
// know we're not in the middle of a UTF8 character.
if ($here === "\r") {
if ($next === "\n") {
$slice_length++;
}
break;
}
// If the next character is 0x7F or lower, or between 0xC2 and 0xF4,
// we're not slicing in the middle of a UTF8 character.
$ord = ord($next);
if ($ord <= 0x7F || ($ord >= 0xC2 && $ord <= 0xF4)) {
break;
}
$slice_length--;
}
$slice = substr($buffer, $pos, $slice_length);
$pos += $slice_length;
$map_bytes += $slice_length;
- $line_count += count(preg_split("/\r\n|\r|\n/", $slice)) - 1;
+
+ // Count newlines in the slice. This goofy approach is meaningfully
+ // faster than "preg_match_all()" or "preg_split()". See PHI766.
+ $n_rn = substr_count($slice, "\r\n");
+ $n_r = substr_count($slice, "\r");
+ $n_n = substr_count($slice, "\n");
+ $line_count += ($n_rn) + ($n_r - $n_rn) + ($n_n - $n_rn);
if ($map_bytes >= ($marker_distance - $max_utf8_width)) {
$map[] = array(
$last_marker + $map_bytes,
$last_count + $line_count,
);
$last_count = $last_count + $line_count;
$line_count = 0;
$last_marker = $last_marker + $map_bytes;
$map_bytes = 0;
$next_marker = $last_marker + $marker_distance;
}
}
$this->lineMap = array(
$map,
$map_bytes,
$line_count,
base64_encode($prefix),
);
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getBuildTarget()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildTarget()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Users must be able to see a build target to view its build log.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->destroyFile($engine);
$this->destroyChunks();
$this->delete();
}
public function destroyFile(PhabricatorDestructionEngine $engine = null) {
if (!$engine) {
$engine = new PhabricatorDestructionEngine();
}
$file_phid = $this->getFilePHID();
if ($file_phid) {
$viewer = $engine->getViewer();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if ($file) {
$engine->destroyObject($file);
}
}
$this->setFilePHID(null);
return $this;
}
public function destroyChunks() {
$chunk = new HarbormasterBuildLogChunk();
$conn = $chunk->establishConnection('w');
// Just delete the chunks directly so we don't have to pull the data over
// the wire for large logs.
queryfx(
$conn,
'DELETE FROM %T WHERE logID = %d',
$chunk->getTableName(),
$this->getID());
return $this;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildTargetPHID')
->setType('phid')
->setDescription(pht('Build target this log is attached to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('byteLength')
->setType('int')
->setDescription(pht('Length of the log in bytes.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('filePHID')
->setType('phid?')
->setDescription(pht('A file containing the log data.')),
);
}
public function getFieldValuesForConduit() {
return array(
'buildTargetPHID' => $this->getBuildTargetPHID(),
'byteLength' => (int)$this->getByteLength(),
'filePHID' => $this->getFilePHID(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php
index 956850bd9..b2f566c3e 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php
@@ -1,187 +1,262 @@
<?php
final class HarbormasterBuildUnitMessage
extends HarbormasterDAO {
protected $buildTargetPHID;
protected $engine;
protected $namespace;
protected $name;
protected $result;
protected $duration;
protected $properties = array();
private $buildTarget = self::ATTACHABLE;
+ const FORMAT_TEXT = 'text';
+ const FORMAT_REMARKUP = 'remarkup';
+
public static function initializeNewUnitMessage(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildUnitMessage())
->setBuildTargetPHID($build_target->getPHID());
}
public static function getParameterSpec() {
return array(
'name' => array(
'type' => 'string',
'description' => pht(
'Short test name, like "ExampleTest".'),
),
'result' => array(
'type' => 'string',
'description' => pht(
'Result of the test.'),
),
'namespace' => array(
'type' => 'optional string',
'description' => pht(
'Optional namespace for this test. This is organizational and '.
'is often a class or module name, like "ExampleTestCase".'),
),
'engine' => array(
'type' => 'optional string',
'description' => pht(
'Test engine running the test, like "JavascriptTestEngine". This '.
'primarily prevents collisions between tests with the same name '.
'in different test suites (for example, a Javascript test and a '.
'Python test).'),
),
'duration' => array(
'type' => 'optional float|int',
'description' => pht(
'Runtime duration of the test, in seconds.'),
),
'path' => array(
'type' => 'optional string',
'description' => pht(
'Path to the file where the test is declared, relative to the '.
'project root.'),
),
'coverage' => array(
'type' => 'optional map<string, wild>',
'description' => pht(
'Coverage information for this test.'),
),
'details' => array(
'type' => 'optional string',
'description' => pht(
'Additional human-readable information about the failure.'),
),
+ 'format' => array(
+ 'type' => 'optional string',
+ 'description' => pht(
+ 'Format for the text provided in "details". Valid values are '.
+ '"text" (default) or "remarkup". This controls how test details '.
+ 'are rendered when shown to users.'),
+ ),
);
}
public static function newFromDictionary(
HarbormasterBuildTarget $build_target,
array $dict) {
$obj = self::initializeNewUnitMessage($build_target);
$spec = self::getParameterSpec();
$spec = ipull($spec, 'type');
// We're just going to ignore extra keys for now, to make it easier to
// add stuff here later on.
$dict = array_select_keys($dict, array_keys($spec));
PhutilTypeSpec::checkMap($dict, $spec);
$obj->setEngine(idx($dict, 'engine', ''));
$obj->setNamespace(idx($dict, 'namespace', ''));
$obj->setName($dict['name']);
$obj->setResult($dict['result']);
$obj->setDuration((float)idx($dict, 'duration'));
$path = idx($dict, 'path');
if (strlen($path)) {
$obj->setProperty('path', $path);
}
$coverage = idx($dict, 'coverage');
if ($coverage) {
$obj->setProperty('coverage', $coverage);
}
$details = idx($dict, 'details');
if ($details) {
$obj->setProperty('details', $details);
}
+ $format = idx($dict, 'format');
+ if ($format) {
+ $obj->setProperty('format', $format);
+ }
+
return $obj;
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'engine' => 'text255',
'namespace' => 'text255',
'name' => 'text255',
'result' => 'text32',
'duration' => 'double?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_target' => array(
'columns' => array('buildTargetPHID'),
),
),
) + parent::getConfiguration();
}
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
}
public function getBuildTarget() {
return $this->assertAttached($this->buildTarget);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getUnitMessageDetails() {
return $this->getProperty('details', '');
}
+ public function getUnitMessageDetailsFormat() {
+ return $this->getProperty('format', self::FORMAT_TEXT);
+ }
+
+ public function newUnitMessageDetailsView(
+ PhabricatorUser $viewer,
+ $summarize = false) {
+
+ $format = $this->getUnitMessageDetailsFormat();
+
+ $is_text = ($format !== self::FORMAT_REMARKUP);
+ $is_remarkup = ($format === self::FORMAT_REMARKUP);
+
+ $full_details = $this->getUnitMessageDetails();
+
+ if (!strlen($full_details)) {
+ if ($summarize) {
+ return null;
+ }
+ $details = phutil_tag('em', array(), pht('No details provided.'));
+ } else if ($summarize) {
+ if ($is_text) {
+ $details = id(new PhutilUTF8StringTruncator())
+ ->setMaximumBytes(2048)
+ ->truncateString($full_details);
+ $details = phutil_split_lines($details);
+
+ $limit = 3;
+ if (count($details) > $limit) {
+ $details = array_slice($details, 0, $limit);
+ }
+
+ $details = implode('', $details);
+ } else {
+ $details = $full_details;
+ }
+ } else {
+ $details = $full_details;
+ }
+
+ require_celerity_resource('harbormaster-css');
+
+ $classes = array();
+ $classes[] = 'harbormaster-unit-details';
+
+ if ($is_remarkup) {
+ $details = new PHUIRemarkupView($viewer, $details);
+ } else {
+ $classes[] = 'harbormaster-unit-details-text';
+ $classes[] = 'PhabricatorMonospaced';
+ }
+
+ return phutil_tag(
+ 'div',
+ array(
+ 'class' => implode(' ', $classes),
+ ),
+ $details);
+ }
+
public function getUnitMessageDisplayName() {
$name = $this->getName();
$namespace = $this->getNamespace();
if (strlen($namespace)) {
$name = $namespace.'::'.$name;
}
$engine = $this->getEngine();
if (strlen($engine)) {
$name = $engine.' > '.$name;
}
if (!strlen($name)) {
return pht('Nameless Test (%d)', $this->getID());
}
return $name;
}
public function getSortKey() {
$status = $this->getResult();
$sort = HarbormasterUnitStatus::getUnitStatusSort($status);
$parts = array(
$sort,
$this->getEngine(),
$this->getNamespace(),
$this->getName(),
$this->getID(),
);
return implode("\0", $parts);
}
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
index b29958882..54c069e97 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
@@ -1,171 +1,184 @@
<?php
final class HarbormasterBuildStep extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $name;
protected $description;
protected $buildPlanPHID;
protected $className;
protected $details = array();
protected $sequence = 0;
protected $stepAutoKey;
private $buildPlan = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $implementation;
public static function initializeNewStep(PhabricatorUser $actor) {
return id(new HarbormasterBuildStep())
->setName('')
->setDescription('');
}
protected 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` should be removed.
'name' => 'text255?',
'stepAutoKey' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_stepautokey' => array(
'columns' => array('buildPlanPHID', 'stepAutoKey'),
'unique' => true,
),
),
) + 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;
}
public function isAutostep() {
return ($this->getStepAutoKey() !== null);
}
+ public function willStartBuild(
+ PhabricatorUser $viewer,
+ HarbormasterBuildable $buildable,
+ HarbormasterBuild $build,
+ HarbormasterBuildPlan $plan) {
+ return $this->getStepImplementation()->willStartBuild(
+ $viewer,
+ $buildable,
+ $build,
+ $plan,
+ $this);
+ }
+
/* -( 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,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
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/harbormaster/view/HarbormasterUnitPropertyView.php b/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php
index 0ce168802..ee8b80a7a 100644
--- a/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php
+++ b/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php
@@ -1,182 +1,157 @@
<?php
final class HarbormasterUnitPropertyView extends AphrontView {
private $pathURIMap = array();
private $unitMessages = array();
private $limit;
private $fullResultsURI;
private $notice;
public function setPathURIMap(array $map) {
$this->pathURIMap = $map;
return $this;
}
public function setUnitMessages(array $messages) {
assert_instances_of($messages, 'HarbormasterBuildUnitMessage');
$this->unitMessages = $messages;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setFullResultsURI($full_results_uri) {
$this->fullResultsURI = $full_results_uri;
return $this;
}
public function setNotice($notice) {
$this->notice = $notice;
return $this;
}
-
public function render() {
- require_celerity_resource('harbormaster-css');
+ $viewer = $this->getViewer();
$messages = $this->unitMessages;
$messages = msort($messages, 'getSortKey');
$limit = $this->limit;
if ($this->limit) {
$display_messages = array_slice($messages, 0, $limit);
} else {
$display_messages = $messages;
}
$rows = array();
$any_duration = false;
foreach ($display_messages as $message) {
$status = $message->getResult();
$icon_icon = HarbormasterUnitStatus::getUnitStatusIcon($status);
$icon_color = HarbormasterUnitStatus::getUnitStatusColor($status);
$icon_label = HarbormasterUnitStatus::getUnitStatusLabel($status);
$result_icon = id(new PHUIIconView())
->setIcon("{$icon_icon} {$icon_color}")
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $icon_label,
));
$duration = $message->getDuration();
if ($duration !== null) {
$any_duration = true;
$duration = pht('%s ms', new PhutilNumber((int)(1000 * $duration)));
}
$name = $message->getUnitMessageDisplayName();
$id = $message->getID();
if ($id) {
$name = phutil_tag(
'a',
array(
'href' => "/harbormaster/unit/view/{$id}/",
),
$name);
}
- $details = $message->getUnitMessageDetails();
- if (strlen($details)) {
- $name = array(
- $name,
- $this->renderUnitTestDetails($details),
- );
- }
+ $name = array(
+ $name,
+ $message->newUnitMessageDetailsView($viewer, true),
+ );
$rows[] = array(
$result_icon,
$duration,
$name,
);
}
$full_uri = $this->fullResultsURI;
if ($full_uri && (count($messages) > $limit)) {
$counts = array();
$groups = mgroup($messages, 'getResult');
foreach ($groups as $status => $group) {
$counts[] = HarbormasterUnitStatus::getUnitStatusCountLabel(
$status,
count($group));
}
$link_text = pht(
'View Full Test Results (%s)',
implode(" \xC2\xB7 ", $counts));
$full_link = phutil_tag(
'a',
array(
'href' => $full_uri,
),
$link_text);
$link_icon = id(new PHUIIconView())
->setIcon('fa-ellipsis-h lightgreytext');
$rows[] = array($link_icon, null, $full_link);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Time'),
pht('Test'),
))
->setColumnClasses(
array(
'top center',
'top right',
'top wide',
))
->setColumnWidths(
array(
'32px',
'64px',
))
->setColumnVisibility(
array(
true,
$any_duration,
));
if ($this->notice) {
$table->setNotice($this->notice);
}
return $table;
}
- private function renderUnitTestDetails($full_details) {
- $details = id(new PhutilUTF8StringTruncator())
- ->setMaximumBytes(2048)
- ->truncateString($full_details);
- $details = phutil_split_lines($details);
-
- $limit = 3;
- if (count($details) > $limit) {
- $details = array_slice($details, 0, $limit);
- }
-
- $details = implode('', $details);
-
- return phutil_tag(
- 'div',
- array(
- 'class' => 'PhabricatorMonospaced harbormaster-unit-details',
- ),
- $details);
- }
-
}
diff --git a/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php b/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php
index e5a66a3eb..66ba2307b 100644
--- a/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php
+++ b/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php
@@ -1,110 +1,111 @@
<?php
final class HarbormasterUnitSummaryView extends AphrontView {
private $buildable;
private $messages;
private $limit;
private $excuse;
private $showViewAll;
public function setBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function setUnitMessages(array $messages) {
$this->messages = $messages;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setExcuse($excuse) {
$this->excuse = $excuse;
return $this;
}
public function setShowViewAll($show_view_all) {
$this->showViewAll = $show_view_all;
return $this;
}
public function render() {
$messages = $this->messages;
$buildable = $this->buildable;
$id = $buildable->getID();
$full_uri = "/harbormaster/unit/{$id}/";
$messages = msort($messages, 'getSortKey');
$head_unit = head($messages);
if ($head_unit) {
$status = $head_unit->getResult();
$tag_text = HarbormasterUnitStatus::getUnitStatusLabel($status);
$tag_color = HarbormasterUnitStatus::getUnitStatusColor($status);
$tag_icon = HarbormasterUnitStatus::getUnitStatusIcon($status);
} else {
$tag_text = pht('No Unit Tests');
$tag_color = 'grey';
$tag_icon = 'fa-ban';
}
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor($tag_color)
->setIcon($tag_icon)
->setName($tag_text);
$header = id(new PHUIHeaderView())
->setHeader(array(pht('Unit Tests'), $tag));
if ($this->showViewAll) {
$view_all = id(new PHUIButtonView())
->setTag('a')
->setHref($full_uri)
->setIcon('fa-list-ul')
->setText('View All');
$header->addActionLink($view_all);
}
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$table = id(new HarbormasterUnitPropertyView())
+ ->setViewer($this->getViewer())
->setUnitMessages($messages);
if ($this->showViewAll) {
$table->setFullResultsURI($full_uri);
}
if ($this->limit) {
$table->setLimit($this->limit);
}
$excuse = $this->excuse;
if (strlen($excuse)) {
$excuse_icon = id(new PHUIIconView())
->setIcon('fa-commenting-o red');
$table->setNotice(
array(
$excuse_icon,
' ',
phutil_tag('strong', array(), pht('Excuse:')),
' ',
$excuse,
));
}
$box->setTable($table);
return $box;
}
}
diff --git a/src/applications/harbormaster/worker/HarbormasterTargetWorker.php b/src/applications/harbormaster/worker/HarbormasterTargetWorker.php
index fa3a01272..a6860f579 100644
--- a/src/applications/harbormaster/worker/HarbormasterTargetWorker.php
+++ b/src/applications/harbormaster/worker/HarbormasterTargetWorker.php
@@ -1,112 +1,118 @@
<?php
/**
* Execute a build target.
*/
final class HarbormasterTargetWorker extends HarbormasterWorker {
public function getRequiredLeaseTime() {
// This worker performs actual build work, which may involve a long wait
// on external systems.
return phutil_units('24 hours in seconds');
}
public function renderForDisplay(PhabricatorUser $viewer) {
try {
$target = $this->loadBuildTarget();
} catch (Exception $ex) {
return null;
}
return $viewer->renderHandle($target->getPHID());
}
private function loadBuildTarget() {
$data = $this->getTaskData();
$id = idx($data, 'targetID');
$target = id(new HarbormasterBuildTargetQuery())
->withIDs(array($id))
->setViewer($this->getViewer())
->needBuildSteps(true)
->executeOne();
if (!$target) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Bad build target ID "%d".',
$id));
}
return $target;
}
protected function doWork() {
$target = $this->loadBuildTarget();
$build = $target->getBuild();
$viewer = $this->getViewer();
- $target->setDateStarted(time());
+ // If this is the first time we're starting work on this target, mark the
+ // current time as the start time. If the target yields or waits, we may
+ // end up here again later, so we don't want to overwrite the start time if
+ // we already have a value.
+ if (!$target->getDateStarted()) {
+ $target->setDateStarted(PhabricatorTime::getNow());
+ }
try {
if ($target->getBuildGeneration() !== $build->getBuildGeneration()) {
throw new HarbormasterBuildAbortedException();
}
$status_pending = HarbormasterBuildTarget::STATUS_PENDING;
if ($target->getTargetStatus() == $status_pending) {
$target->setTargetStatus(HarbormasterBuildTarget::STATUS_BUILDING);
$target->save();
}
$implementation = $target->getImplementation();
$implementation->setCurrentWorkerTaskID($this->getCurrentWorkerTaskID());
$implementation->execute($build, $target);
$next_status = HarbormasterBuildTarget::STATUS_PASSED;
if ($implementation->shouldWaitForMessage($target)) {
$next_status = HarbormasterBuildTarget::STATUS_WAITING;
}
$target->setTargetStatus($next_status);
if ($target->isComplete()) {
$target->setDateCompleted(PhabricatorTime::getNow());
}
$target->save();
} catch (PhabricatorWorkerYieldException $ex) {
// If the target wants to yield, let that escape without further
// processing. We'll resume after the task retries.
throw $ex;
} catch (HarbormasterBuildFailureException $ex) {
// A build step wants to fail explicitly.
$target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED);
$target->setDateCompleted(PhabricatorTime::getNow());
$target->save();
} catch (HarbormasterBuildAbortedException $ex) {
// A build step is aborting because the build has been restarted.
$target->setTargetStatus(HarbormasterBuildTarget::STATUS_ABORTED);
$target->setDateCompleted(PhabricatorTime::getNow());
$target->save();
} catch (Exception $ex) {
try {
$log = $target->newLog('core', 'exception')
->append($ex)
->closeBuildLog();
} catch (Exception $log_ex) {
phlog($log_ex);
}
$target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED);
$target->setDateCompleted(time());
$target->save();
}
id(new HarbormasterBuildEngine())
->setViewer($viewer)
->setBuild($build)
->continueBuild();
}
}
diff --git a/src/applications/herald/adapter/HeraldRuleAdapter.php b/src/applications/herald/adapter/HeraldRuleAdapter.php
new file mode 100644
index 000000000..8ed851a1a
--- /dev/null
+++ b/src/applications/herald/adapter/HeraldRuleAdapter.php
@@ -0,0 +1,74 @@
+<?php
+
+final class HeraldRuleAdapter extends HeraldAdapter {
+
+ private $rule;
+
+ protected function newObject() {
+ return new HeraldRule();
+ }
+
+ public function getAdapterApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ public function getAdapterContentDescription() {
+ return pht('React to Herald rules being created or updated.');
+ }
+
+ public function isTestAdapterForObject($object) {
+ return ($object instanceof HeraldRule);
+ }
+
+ public function getAdapterTestDescription() {
+ return pht(
+ 'Test rules which run when another Herald rule is created or '.
+ 'updated.');
+ }
+
+ protected function initializeNewAdapter() {
+ $this->rule = $this->newObject();
+ }
+
+ public function supportsApplicationEmail() {
+ return true;
+ }
+
+ public function supportsRuleType($rule_type) {
+ switch ($rule_type) {
+ case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
+ case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
+ return true;
+ case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
+ default:
+ return false;
+ }
+ }
+
+ public function setRule(HeraldRule $rule) {
+ $this->rule = $rule;
+ return $this;
+ }
+
+ public function getRule() {
+ return $this->rule;
+ }
+
+ public function setObject($object) {
+ $this->rule = $object;
+ return $this;
+ }
+
+ public function getObject() {
+ return $this->rule;
+ }
+
+ public function getAdapterContentName() {
+ return pht('Herald Rules');
+ }
+
+ public function getHeraldName() {
+ return $this->getRule()->getMonogram();
+ }
+
+}
diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php
index 4ddab2669..5c2d26cf5 100644
--- a/src/applications/herald/controller/HeraldTestConsoleController.php
+++ b/src/applications/herald/controller/HeraldTestConsoleController.php
@@ -1,221 +1,248 @@
<?php
final class HeraldTestConsoleController extends HeraldController {
private $testObject;
private $testAdapter;
public function setTestObject($test_object) {
$this->testObject = $test_object;
return $this;
}
public function getTestObject() {
return $this->testObject;
}
public function setTestAdapter(HeraldAdapter $test_adapter) {
$this->testAdapter = $test_adapter;
return $this;
}
public function getTestAdapter() {
return $this->testAdapter;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$response = $this->loadTestObject($request);
if ($response) {
return $response;
}
$response = $this->loadAdapter($request);
if ($response) {
return $response;
}
$object = $this->getTestObject();
$adapter = $this->getTestAdapter();
+ $source = $this->newContentSource($object);
$adapter
+ ->setContentSource($source)
->setIsNewObject(false)
->setActingAsPHID($viewer->getPHID())
->setViewer($viewer);
$rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withContentTypes(array($adapter->getAdapterContentType()))
->withDisabled(false)
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($object->getPHID()))
->needValidateAuthors(true)
->execute();
$engine = id(new HeraldEngine())
->setDryRun(true);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
$xscript = $engine->getTranscript();
return id(new AphrontRedirectResponse())
->setURI('/herald/transcript/'.$xscript->getID().'/');
}
private function loadTestObject(AphrontRequest $request) {
$viewer = $this->getViewer();
$e_name = true;
$v_name = null;
$errors = array();
if ($request->isFormPost()) {
$v_name = trim($request->getStr('object_name'));
if (!$v_name) {
$e_name = pht('Required');
$errors[] = pht('An object name is required.');
}
if (!$errors) {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($v_name))
->executeOne();
if (!$object) {
$e_name = pht('Invalid');
$errors[] = pht('No object exists with that name.');
}
}
if (!$errors) {
$this->setTestObject($object);
return null;
}
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
'Enter an object to test rules for, like a Diffusion commit (e.g., '.
'`rX123`) or a Differential revision (e.g., `D123`). You will be '.
'shown the results of a dry run on the object.'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Object Name'))
->setName('object_name')
->setError($e_name)
->setValue($v_name))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue')));
return $this->buildTestConsoleResponse($form, $errors);
}
private function loadAdapter(AphrontRequest $request) {
$viewer = $this->getViewer();
$object = $this->getTestObject();
$adapter_key = $request->getStr('adapter');
$adapters = HeraldAdapter::getAllAdapters();
$can_select = array();
$display_adapters = array();
foreach ($adapters as $key => $adapter) {
if (!$adapter->isTestAdapterForObject($object)) {
continue;
}
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$display_adapters[$key] = $adapter;
if ($adapter->canCreateTestAdapterForObject($object)) {
$can_select[$key] = $adapter;
}
}
if ($request->isFormPost() && $adapter_key) {
if (isset($can_select[$adapter_key])) {
$adapter = $can_select[$adapter_key]->newTestAdapter(
$viewer,
$object);
$this->setTestAdapter($adapter);
return null;
}
}
$form = id(new AphrontFormView())
->addHiddenInput('object_name', $request->getStr('object_name'))
->setViewer($viewer);
$cancel_uri = $this->getApplicationURI();
if (!$display_adapters) {
$form
->appendRemarkupInstructions(
pht('//There are no available Herald events for this object.//'))
->appendControl(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri));
} else {
$adapter_control = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Event'))
->setName('adapter')
->setValue(head_key($can_select));
foreach ($display_adapters as $adapter_key => $adapter) {
$is_disabled = empty($can_select[$adapter_key]);
$adapter_control->addButton(
$adapter_key,
$adapter->getAdapterContentName(),
$adapter->getAdapterTestDescription(),
null,
$is_disabled);
}
$form
->appendControl($adapter_control)
->appendControl(
id(new AphrontFormSubmitControl())
->setValue(pht('Run Test')));
}
return $this->buildTestConsoleResponse($form, array());
}
private function buildTestConsoleResponse($form, array $errors) {
$box = id(new PHUIObjectBoxView())
->setFormErrors($errors)
->setForm($form);
$crumbs = id($this->buildApplicationCrumbs())
->addTextCrumb(pht('Test Console'))
->setBorder(true);
$title = pht('Test Console');
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-desktop');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
+ private function newContentSource($object) {
+ $viewer = $this->getViewer();
+
+ // Try using the content source associated with the most recent transaction
+ // on the object.
+
+ $query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
+
+ $xaction = $query
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object->getPHID()))
+ ->setLimit(1)
+ ->setOrder('newest')
+ ->executeOne();
+ if ($xaction) {
+ return $xaction->getContentSource();
+ }
+
+ // If we couldn't find a transaction (which should be rare), fall back to
+ // building a new content source from the test console request itself.
+
+ $request = $this->getRequest();
+ return PhabricatorContentSource::newFromRequest($request);
+ }
+
}
diff --git a/src/applications/herald/editor/HeraldRuleEditor.php b/src/applications/herald/editor/HeraldRuleEditor.php
index 30480108f..3ba5c4f8a 100644
--- a/src/applications/herald/editor/HeraldRuleEditor.php
+++ b/src/applications/herald/editor/HeraldRuleEditor.php
@@ -1,90 +1,154 @@
<?php
final class HeraldRuleEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorHeraldApplication';
}
public function getEditorObjectsDescription() {
return pht('Herald Rules');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = HeraldRuleTransaction::TYPE_EDIT;
$types[] = HeraldRuleTransaction::TYPE_NAME;
$types[] = HeraldRuleTransaction::TYPE_DISABLE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_DISABLE:
return (int)$object->getIsDisabled();
case HeraldRuleTransaction::TYPE_EDIT:
return id(new HeraldRuleSerializer())
->serializeRule($object);
case HeraldRuleTransaction::TYPE_NAME:
return $object->getName();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_DISABLE:
return (int)$xaction->getNewValue();
case HeraldRuleTransaction::TYPE_EDIT:
case HeraldRuleTransaction::TYPE_NAME:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_DISABLE:
return $object->setIsDisabled($xaction->getNewValue());
case HeraldRuleTransaction::TYPE_NAME:
return $object->setName($xaction->getNewValue());
case HeraldRuleTransaction::TYPE_EDIT:
$new_state = id(new HeraldRuleSerializer())
->deserializeRuleComponents($xaction->getNewValue());
$object->setMustMatchAll((int)$new_state['match_all']);
$object->attachConditions($new_state['conditions']);
$object->attachActions($new_state['actions']);
$new_repetition = $new_state['repetition_policy'];
$object->setRepetitionPolicyStringConstant($new_repetition);
return $object;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_EDIT:
$object->saveConditions($object->getConditions());
$object->saveActions($object->getActions());
break;
}
return;
}
+ protected function shouldApplyHeraldRules(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+ return true;
+ }
+
+ protected function buildHeraldAdapter(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+ return id(new HeraldRuleAdapter())
+ ->setRule($object);
+ }
+
+ protected function shouldSendMail(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+ return true;
+ }
+
+ protected function getMailTo(PhabricatorLiskDAO $object) {
+ $phids = array();
+
+ $phids[] = $this->getActingAsPHID();
+
+ if ($object->isPersonalRule()) {
+ $phids[] = $object->getAuthorPHID();
+ }
+
+ return $phids;
+ }
+
+ protected function buildReplyHandler(PhabricatorLiskDAO $object) {
+ return id(new HeraldRuleReplyHandler())
+ ->setMailReceiver($object);
+ }
+
+ protected function buildMailTemplate(PhabricatorLiskDAO $object) {
+ $monogram = $object->getMonogram();
+ $name = $object->getName();
+
+ $subject = pht('%s: %s', $monogram, $name);
+
+ return id(new PhabricatorMetaMTAMail())
+ ->setSubject($subject);
+ }
+
+ protected function getMailSubjectPrefix() {
+ return pht('[Herald]');
+ }
+
+
+ protected function buildMailBody(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+
+ $body = parent::buildMailBody($object, $xactions);
+
+ $body->addLinkSection(
+ pht('RULE DETAIL'),
+ PhabricatorEnv::getProductionURI($object->getURI()));
+
+ return $body;
+ }
+
}
diff --git a/src/applications/herald/field/rule/HeraldRuleAdapterField.php b/src/applications/herald/field/rule/HeraldRuleAdapterField.php
new file mode 100644
index 000000000..bcb57f520
--- /dev/null
+++ b/src/applications/herald/field/rule/HeraldRuleAdapterField.php
@@ -0,0 +1,29 @@
+<?php
+
+final class HeraldRuleAdapterField
+ extends HeraldRuleField {
+
+ const FIELDCONST = 'adapter';
+
+ public function getHeraldFieldName() {
+ return pht('Content type');
+ }
+
+ public function getHeraldFieldValue($object) {
+ return $object->getContentType();
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID;
+ }
+
+ protected function getDatasource() {
+ return new HeraldAdapterDatasource();
+ }
+
+ protected function getDatasourceValueMap() {
+ $adapters = HeraldAdapter::getAllAdapters();
+ return mpull($adapters, 'getAdapterContentName', 'getAdapterContentType');
+ }
+
+}
diff --git a/src/applications/herald/field/rule/HeraldRuleField.php b/src/applications/herald/field/rule/HeraldRuleField.php
new file mode 100644
index 000000000..00ade268b
--- /dev/null
+++ b/src/applications/herald/field/rule/HeraldRuleField.php
@@ -0,0 +1,14 @@
+<?php
+
+abstract class HeraldRuleField
+ extends HeraldField {
+
+ public function getFieldGroupKey() {
+ return ManiphestTaskHeraldFieldGroup::FIELDGROUPKEY;
+ }
+
+ public function supportsObject($object) {
+ return ($object instanceof HeraldRule);
+ }
+
+}
diff --git a/src/applications/herald/field/rule/HeraldRuleFieldGroup.php b/src/applications/herald/field/rule/HeraldRuleFieldGroup.php
new file mode 100644
index 000000000..ac9b54035
--- /dev/null
+++ b/src/applications/herald/field/rule/HeraldRuleFieldGroup.php
@@ -0,0 +1,16 @@
+<?php
+
+final class HeraldRuleFieldGroup
+ extends HeraldFieldGroup {
+
+ const FIELDGROUPKEY = 'herald.rule';
+
+ public function getGroupLabel() {
+ return pht('Rule Fields');
+ }
+
+ protected function getGroupOrder() {
+ return 500;
+ }
+
+}
diff --git a/src/applications/herald/field/rule/HeraldRuleTypeField.php b/src/applications/herald/field/rule/HeraldRuleTypeField.php
new file mode 100644
index 000000000..1a367c0a8
--- /dev/null
+++ b/src/applications/herald/field/rule/HeraldRuleTypeField.php
@@ -0,0 +1,28 @@
+<?php
+
+final class HeraldRuleTypeField
+ extends HeraldRuleField {
+
+ const FIELDCONST = 'rule-type';
+
+ public function getHeraldFieldName() {
+ return pht('Rule type');
+ }
+
+ public function getHeraldFieldValue($object) {
+ return $object->getRuleType();
+ }
+
+ protected function getHeraldFieldStandardType() {
+ return self::STANDARD_PHID;
+ }
+
+ protected function getDatasource() {
+ return new HeraldRuleTypeDatasource();
+ }
+
+ protected function getDatasourceValueMap() {
+ return HeraldRuleTypeConfig::getRuleTypeMap();
+ }
+
+}
diff --git a/src/applications/herald/mail/HeraldRuleReplyHandler.php b/src/applications/herald/mail/HeraldRuleReplyHandler.php
new file mode 100644
index 000000000..c4bf9fad4
--- /dev/null
+++ b/src/applications/herald/mail/HeraldRuleReplyHandler.php
@@ -0,0 +1,16 @@
+<?php
+
+final class HeraldRuleReplyHandler
+ extends PhabricatorApplicationTransactionReplyHandler {
+
+ public function validateMailReceiver($mail_receiver) {
+ if (!($mail_receiver instanceof HeraldRule)) {
+ throw new Exception(pht('Mail receiver is not a %s!', 'HeraldRule'));
+ }
+ }
+
+ public function getObjectPrefix() {
+ return 'H';
+ }
+
+}
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index 44ef68ac0..b49deb02d 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,400 +1,456 @@
<?php
final class HeraldRule extends HeraldDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorSubscribableInterface {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';
protected $name;
protected $authorPHID;
protected $contentType;
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $isDisabled = 0;
protected $triggerObjectPHID;
protected $configVersion = 38;
// PHIDs for which this rule has been applied
private $ruleApplied = self::ATTACHABLE;
private $validAuthor = self::ATTACHABLE;
private $author = self::ATTACHABLE;
private $conditions;
private $actions;
private $triggerObject = self::ATTACHABLE;
const REPEAT_EVERY = 'every';
const REPEAT_FIRST = 'first';
const REPEAT_CHANGE = 'change';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'contentType' => 'text255',
'mustMatchAll' => 'bool',
'configVersion' => 'uint32',
'repetitionPolicy' => 'text32',
'ruleType' => 'text32',
'isDisabled' => 'uint32',
'triggerObjectPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('name(128)'),
),
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_ruletype' => array(
'columns' => array('ruleType'),
),
'key_trigger' => array(
'columns' => array('triggerObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST);
}
public function getRuleApplied($phid) {
return $this->assertAttachedKey($this->ruleApplied, $phid);
}
public function setRuleApplied($phid, $applied) {
if ($this->ruleApplied === self::ATTACHABLE) {
$this->ruleApplied = array();
}
$this->ruleApplied[$phid] = $applied;
return $this;
}
public function loadConditions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldCondition())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
$this->conditions = $conditions;
return $this;
}
public function getConditions() {
// TODO: validate conditions have been attached.
return $this->conditions;
}
public function loadActions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldActionRecord())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachActions(array $actions) {
// TODO: validate actions have been attached.
assert_instances_of($actions, 'HeraldActionRecord');
$this->actions = $actions;
return $this;
}
public function getActions() {
return $this->actions;
}
public function saveConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
return $this->saveChildren(
id(new HeraldCondition())->getTableName(),
$conditions);
}
public function saveActions(array $actions) {
assert_instances_of($actions, 'HeraldActionRecord');
return $this->saveChildren(
id(new HeraldActionRecord())->getTableName(),
$actions);
}
protected function saveChildren($table_name, array $children) {
assert_instances_of($children, 'HeraldDAO');
if (!$this->getID()) {
throw new PhutilInvalidStateException('save');
}
foreach ($children as $child) {
$child->setRuleID($this->getID());
}
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
$table_name,
$this->getID());
foreach ($children as $child) {
$child->save();
}
$this->saveTransaction();
}
public function delete() {
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldCondition())->getTableName(),
$this->getID());
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldActionRecord())->getTableName(),
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function hasValidAuthor() {
return $this->assertAttached($this->validAuthor);
}
public function attachValidAuthor($valid) {
$this->validAuthor = $valid;
return $this;
}
public function getAuthor() {
return $this->assertAttached($this->author);
}
public function attachAuthor(PhabricatorUser $user) {
$this->author = $user;
return $this;
}
public function isGlobalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL);
}
public function isPersonalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function isObjectRule() {
return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
}
public function attachTriggerObject($trigger_object) {
$this->triggerObject = $trigger_object;
return $this;
}
public function getTriggerObject() {
return $this->assertAttached($this->triggerObject);
}
/**
* Get a sortable key for rule execution order.
*
* Rules execute in a well-defined order: personal rules first, then object
* rules, then global rules. Within each rule type, rules execute from lowest
* ID to highest ID.
*
* This ordering allows more powerful rules (like global rules) to override
* weaker rules (like personal rules) when multiple rules exist which try to
* affect the same field. Executing from low IDs to high IDs makes
* interactions easier to understand when adding new rules, because the newest
* rules always happen last.
*
* @return string A sortable key for this rule.
*/
public function getRuleExecutionOrderSortKey() {
$rule_type = $this->getRuleType();
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$type_order = 1;
break;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$type_order = 2;
break;
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$type_order = 3;
break;
default:
throw new Exception(pht('Unknown rule type "%s"!', $rule_type));
}
return sprintf('~%d%010d', $type_order, $this->getID());
}
public function getMonogram() {
return 'H'.$this->getID();
}
+ public function getURI() {
+ return '/'.$this->getMonogram();
+ }
+
+
+/* -( Repetition Policies )------------------------------------------------ */
+
+
+ public function getRepetitionPolicyStringConstant() {
+ return $this->getRepetitionPolicy();
+ }
+
+ public function setRepetitionPolicyStringConstant($value) {
+ $map = self::getRepetitionPolicyMap();
+
+ if (!isset($map[$value])) {
+ throw new Exception(
+ pht(
+ 'Rule repetition string constant "%s" is unknown.',
+ $value));
+ }
+
+ return $this->setRepetitionPolicy($value);
+ }
+
+ public function isRepeatEvery() {
+ return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY);
+ }
+
+ public function isRepeatFirst() {
+ return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST);
+ }
+
+ public function isRepeatOnChange() {
+ return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE);
+ }
+
+ public static function getRepetitionPolicySelectOptionMap() {
+ $map = self::getRepetitionPolicyMap();
+ return ipull($map, 'select');
+ }
+
+ private static function getRepetitionPolicyMap() {
+ return array(
+ self::REPEAT_EVERY => array(
+ 'select' => pht('every time this rule matches:'),
+ ),
+ self::REPEAT_FIRST => array(
+ 'select' => pht('only the first time this rule matches:'),
+ ),
+ self::REPEAT_CHANGE => array(
+ 'select' => pht('if this rule did not match the last time:'),
+ ),
+ );
+ }
+
/* -( Repetition Policies )------------------------------------------------ */
public function getRepetitionPolicyStringConstant() {
return $this->getRepetitionPolicy();
}
public function setRepetitionPolicyStringConstant($value) {
$map = self::getRepetitionPolicyMap();
if (!isset($map[$value])) {
throw new Exception(
pht(
'Rule repetition string constant "%s" is unknown.',
$value));
}
return $this->setRepetitionPolicy($value);
}
public function isRepeatEvery() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY);
}
public function isRepeatFirst() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST);
}
public function isRepeatOnChange() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE);
}
public static function getRepetitionPolicySelectOptionMap() {
$map = self::getRepetitionPolicyMap();
return ipull($map, 'select');
}
private static function getRepetitionPolicyMap() {
return array(
self::REPEAT_EVERY => array(
'select' => pht('every time this rule matches:'),
),
self::REPEAT_FIRST => array(
'select' => pht('only the first time this rule matches:'),
),
self::REPEAT_CHANGE => array(
'select' => pht('if this rule did not match the last time:'),
),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HeraldRuleEditor();
}
public function 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 ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->isGlobalRule()) {
$app = 'PhabricatorHeraldApplication';
$herald = PhabricatorApplication::getByClass($app);
$global = HeraldManageGlobalRulesCapability::CAPABILITY;
return $herald->getPolicy($global);
} else if ($this->isObjectRule()) {
return $this->getTriggerObject()->getPolicy($capability);
} else {
return $this->getAuthorPHID();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return null;
}
if ($this->isGlobalRule()) {
return pht(
'Global Herald rules can be edited by users with the "Can Manage '.
'Global Rules" Herald application permission.');
} else if ($this->isObjectRule()) {
return pht('Object rules inherit the edit policies of their objects.');
} else {
return pht('A personal rule can only be edited by its owner.');
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return $this->isPersonalRule() && $phid == $this->getAuthorPHID();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/herald/typeahead/HeraldAdapterDatasource.php b/src/applications/herald/typeahead/HeraldAdapterDatasource.php
new file mode 100644
index 000000000..1fffd1bb6
--- /dev/null
+++ b/src/applications/herald/typeahead/HeraldAdapterDatasource.php
@@ -0,0 +1,45 @@
+<?php
+
+final class HeraldAdapterDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Herald Adapters');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type an adapter name...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ public function loadResults() {
+ $results = $this->buildResults();
+ return $this->filterResultsAgainstTokens($results);
+ }
+
+ protected function renderSpecialTokens(array $values) {
+ return $this->renderTokensFromResults($this->buildResults(), $values);
+ }
+
+ private function buildResults() {
+ $results = array();
+
+ $adapters = HeraldAdapter::getAllAdapters();
+ foreach ($adapters as $adapter) {
+ $value = $adapter->getAdapterContentType();
+ $name = $adapter->getAdapterContentName();
+
+ $result = id(new PhabricatorTypeaheadResult())
+ ->setPHID($value)
+ ->setName($name);
+
+ $results[$value] = $result;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/herald/typeahead/HeraldRuleDatasource.php b/src/applications/herald/typeahead/HeraldRuleDatasource.php
index c2db05c9e..88343bd43 100644
--- a/src/applications/herald/typeahead/HeraldRuleDatasource.php
+++ b/src/applications/herald/typeahead/HeraldRuleDatasource.php
@@ -1,49 +1,57 @@
<?php
final class HeraldRuleDatasource
extends PhabricatorTypeaheadDatasource {
public function getPlaceholderText() {
return pht('Type a Herald rule name...');
}
public function getBrowseTitle() {
return pht('Browse Herald Rules');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorHeraldApplication';
}
public function loadResults() {
$viewer = $this->getViewer();
$raw_query = $this->getRawQuery();
- $rules = id(new HeraldRuleQuery())
- ->setViewer($viewer)
- ->withDatasourceQuery($raw_query)
- ->execute();
+ $query = id(new HeraldRuleQuery())
+ ->setViewer($viewer);
+
+ if (preg_match('/^[hH]\d+\z/', $raw_query)) {
+ $id = trim($raw_query, 'hH');
+ $id = (int)$id;
+ $query->withIDs(array($id));
+ } else {
+ $query->withDatasourceQuery($raw_query);
+ }
+
+ $rules = $query->execute();
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(mpull($rules, 'getPHID'))
->execute();
$results = array();
foreach ($rules as $rule) {
$handle = $handles[$rule->getPHID()];
$result = id(new PhabricatorTypeaheadResult())
->setName($handle->getFullName())
->setPHID($handle->getPHID());
if ($rule->getIsDisabled()) {
$result->setClosed(pht('Archived'));
}
$results[] = $result;
}
return $results;
}
}
diff --git a/src/applications/herald/typeahead/HeraldRuleTypeDatasource.php b/src/applications/herald/typeahead/HeraldRuleTypeDatasource.php
new file mode 100644
index 000000000..8dfa7d0f6
--- /dev/null
+++ b/src/applications/herald/typeahead/HeraldRuleTypeDatasource.php
@@ -0,0 +1,42 @@
+<?php
+
+final class HeraldRuleTypeDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Rule Types');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type a rule type...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorHeraldApplication';
+ }
+
+ public function loadResults() {
+ $results = $this->buildResults();
+ return $this->filterResultsAgainstTokens($results);
+ }
+
+ protected function renderSpecialTokens(array $values) {
+ return $this->renderTokensFromResults($this->buildResults(), $values);
+ }
+
+ private function buildResults() {
+ $results = array();
+
+ $type_map = HeraldRuleTypeConfig::getRuleTypeMap();
+ foreach ($type_map as $type => $name) {
+ $result = id(new PhabricatorTypeaheadResult())
+ ->setPHID($type)
+ ->setName($name);
+
+ $results[$type] = $result;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
index f19cf43d0..1748dba45 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
@@ -1,701 +1,701 @@
<?php
final class LegalpadDocumentSignController extends LegalpadController {
public function shouldAllowPublic() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$document = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needDocumentBodies(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$information = $this->readSignerInformation(
$document,
$request);
if ($information instanceof AphrontResponse) {
return $information;
}
list($signer_phid, $signature_data) = $information;
$signature = null;
$type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
$is_individual = ($document->getSignatureType() == $type_individual);
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// nothing to sign means this should be true
$has_signed = true;
// this is a status UI element
$signed_status = null;
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
if ($signer_phid) {
// TODO: This is odd and should probably be adjusted after
// grey/external accounts work better, but use the omnipotent
// viewer to check for a signature so we can pick up
// anonymous/grey signatures.
$signature = id(new LegalpadDocumentSignatureQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDocumentPHIDs(array($document->getPHID()))
->withSignerPHIDs(array($signer_phid))
->executeOne();
if ($signature && !$viewer->isLoggedIn()) {
return $this->newDialog()
->setTitle(pht('Already Signed'))
->appendParagraph(pht('You have already signed this document!'))
->addCancelButton('/'.$document->getMonogram(), pht('Okay'));
}
}
$signed_status = null;
if (!$signature) {
$has_signed = false;
$signature = id(new LegalpadDocumentSignature())
->setSignerPHID($signer_phid)
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
// If the user is logged in, show a notice that they haven't signed.
// If they aren't logged in, we can't be as sure, so don't show
// anything.
if ($viewer->isLoggedIn()) {
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht('You have not signed this document yet.'),
));
}
} else {
$has_signed = true;
$signature_data = $signature->getSignatureData();
// In this case, we know they've signed.
$signed_at = $signature->getDateCreated();
if ($signature->getIsExemption()) {
$exemption_phid = $signature->getExemptionPHID();
$handles = $this->loadViewerHandles(array($exemption_phid));
$exemption_handle = $handles[$exemption_phid];
$signed_text = pht(
'You do not need to sign this document. '.
'%s added a signature exemption for you on %s.',
$exemption_handle->renderLink(),
phabricator_datetime($signed_at, $viewer));
} else {
$signed_text = pht(
'You signed this document on %s.',
phabricator_datetime($signed_at, $viewer));
}
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(array($signed_text));
}
$field_errors = array(
'name' => true,
'email' => true,
'agree' => true,
);
$signature->setSignatureData($signature_data);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signature = id(new LegalpadDocumentSignature())
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
if ($viewer->isLoggedIn()) {
$has_signed = false;
$signed_status = null;
} else {
// This just hides the form.
$has_signed = true;
$login_text = pht(
'This document requires a corporate signatory. You must log in to '.
'accept this document on behalf of a company you represent.');
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($login_text));
}
$field_errors = array(
'name' => true,
'address' => true,
'contact.name' => true,
'email' => true,
);
$signature->setSignatureData($signature_data);
break;
}
$errors = array();
if ($request->isFormOrHisecPost() && !$has_signed) {
// Require two-factor auth to sign legal documents.
if ($viewer->isLoggedIn()) {
$engine = new PhabricatorAuthSessionEngine();
$engine->requireHighSecuritySession(
$viewer,
$request,
'/'.$document->getMonogram());
}
list($form_data, $errors, $field_errors) = $this->readSignatureForm(
$document,
$request);
$signature_data = $form_data + $signature_data;
$signature->setSignatureData($signature_data);
$signature->setSignatureType($document->getSignatureType());
$signature->setSignerName((string)idx($signature_data, 'name'));
$signature->setSignerEmail((string)idx($signature_data, 'email'));
$agree = $request->getExists('agree');
if (!$agree) {
$errors[] = pht(
'You must check "I agree to the terms laid forth above."');
$field_errors['agree'] = pht('Required');
}
if ($viewer->isLoggedIn() && $is_individual) {
$verified = LegalpadDocumentSignature::VERIFIED;
} else {
$verified = LegalpadDocumentSignature::UNVERIFIED;
}
$signature->setVerified($verified);
if (!$errors) {
$signature->save();
// If the viewer is logged in, signing for themselves, send them to
// the document page, which will show that they have signed the
// document. Unless of course they were required to sign the
// document to use Phabricator; in that case try really hard to
// re-direct them to where they wanted to go.
//
// Otherwise, send them to a completion page.
if ($viewer->isLoggedIn() && $is_individual) {
$next_uri = '/'.$document->getMonogram();
if ($document->getRequireSignature()) {
$request_uri = $request->getRequestURI();
$next_uri = (string)$request_uri;
}
} else {
$this->sendVerifySignatureEmail(
$document,
$signature);
$next_uri = $this->getApplicationURI('done/');
}
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
}
$document_body = $document->getDocumentBody();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer);
$engine->addObject(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$engine->process();
$document_markup = $engine->getOutput(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$title = $document_body->getTitle();
$manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$document,
PhabricatorPolicyCapability::CAN_EDIT);
// Use the last content update as the modified date. We don't want to
// show that a document like a TOS was "updated" by an incidental change
// to a field like the preamble or privacy settings which does not actually
// affect the content of the agreement.
$content_updated = $document_body->getDateCreated();
// NOTE: We're avoiding `setPolicyObject()` here so we don't pick up
// extra UI elements that are unnecessary and clutter the signature page.
// These details are available on the "Manage" page.
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setEpoch($content_updated)
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-pencil')
->setText(pht('Manage'))
->setHref($manage_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$preamble_box = null;
if (strlen($document->getPreamble())) {
$preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());
// NOTE: We're avoiding `setObject()` here so we don't pick up extra UI
// elements like "Subscribers". This information is available on the
// "Manage" page, but just clutters up the "Signature" page.
$preamble = id(new PHUIPropertyListView())
->setUser($viewer)
->addSectionHeader(pht('Preamble'))
->addTextContent($preamble_text);
$preamble_box = new PHUIPropertyGroupView();
$preamble_box->addPropertyList($preamble);
}
- $content = id(new PHUIDocumentViewPro())
+ $content = id(new PHUIDocumentView())
->addClass('legalpad')
->setHeader($header)
->appendChild(
array(
$signed_status,
$preamble_box,
$document_markup,
));
$signature_box = null;
if (!$has_signed) {
$error_view = null;
if ($errors) {
$error_view = id(new PHUIInfoView())
->setErrors($errors);
}
$signature_form = $this->buildSignatureForm(
$document,
$signature,
$field_errors);
switch ($document->getSignatureType()) {
default:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$box = id(new PHUIObjectBoxView())
->addClass('document-sign-box')
->setHeaderText(pht('Agree and Sign Document'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($signature_form);
if ($error_view) {
$box->setInfoView($error_view);
}
$signature_box = phutil_tag_div(
'phui-document-view-pro-box plt', $box);
break;
}
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb($document->getMonogram());
$box = id(new PHUITwoColumnView())
->setFooter($signature_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($document->getPHID()))
->appendChild(array(
$content,
$box,
));
}
private function readSignerInformation(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
$signer_phid = null;
$signature_data = array();
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
if ($viewer->isLoggedIn()) {
$signer_phid = $viewer->getPHID();
$signature_data = array(
'name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
);
} else if ($request->isFormPost()) {
$email = new PhutilEmailAddress($request->getStr('email'));
if (strlen($email->getDomainName())) {
$email_obj = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $email->getAddress());
if ($email_obj) {
return $this->signInResponse();
}
$external_account = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withAccountTypes(array('email'))
->withAccountDomains(array($email->getDomainName()))
->withAccountIDs(array($email->getAddress()))
->loadOneOrCreate();
if ($external_account->getUserPHID()) {
return $this->signInResponse();
}
$signer_phid = $external_account->getPHID();
}
}
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signer_phid = $viewer->getPHID();
if ($signer_phid) {
$signature_data = array(
'contact.name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
'actorPHID' => $viewer->getPHID(),
);
}
break;
}
return array($signer_phid, $signature_data);
}
private function buildSignatureForm(
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$viewer = $this->getRequest()->getUser();
$data = $signature->getSignatureData();
$form = id(new AphrontFormView())
->setUser($viewer);
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// bail out of here quick
return;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$this->buildIndividualSignatureForm(
$form,
$document,
$signature,
$errors);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$this->buildCorporateSignatureForm(
$form,
$document,
$signature,
$errors);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->setError(idx($errors, 'agree', null))
->addCheckbox(
'agree',
'agree',
pht('I agree to the terms laid forth above.'),
false));
if ($document->getRequireSignature()) {
$cancel_uri = '/logout/';
$cancel_text = pht('Log Out');
} else {
$cancel_uri = $this->getApplicationURI();
$cancel_text = pht('Cancel');
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Sign Document'))
->addCancelButton($cancel_uri, $cancel_text));
return $form;
}
private function buildIndividualSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)));
$viewer = $this->getRequest()->getUser();
if (!$viewer->isLoggedIn()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
}
return $form;
}
private function buildCorporateSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Company Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Company Address'))
->setValue(idx($data, 'address', ''))
->setName('address')
->setError(idx($errors, 'address', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Name'))
->setValue(idx($data, 'contact.name', ''))
->setName('contact.name')
->setError(idx($errors, 'contact.name', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
return $form;
}
private function readSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$result = $this->readIndividualSignatureForm(
$document,
$request);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$result = $this->readCorporateSignatureForm(
$document,
$request);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
return $result;
}
private function readIndividualSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Name field is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
$email = $viewer->loadPrimaryEmailAddress();
} else {
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Email field is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function readCorporateSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
throw new Exception(
pht(
'You can not sign a document on behalf of a corporation unless '.
'you are logged in.'));
}
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Company name is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$address = $request->getStr('address');
if (!strlen($address)) {
$field_errors['address'] = pht('Required');
$errors[] = pht('Company address is required.');
} else {
$field_errors['address'] = null;
}
$signature_data['address'] = $address;
$contact_name = $request->getStr('contact.name');
if (!strlen($contact_name)) {
$field_errors['contact.name'] = pht('Required');
$errors[] = pht('Contact name is required.');
} else {
$field_errors['contact.name'] = null;
}
$signature_data['contact.name'] = $contact_name;
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Contact email is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function sendVerifySignatureEmail(
LegalpadDocument $doc,
LegalpadDocumentSignature $signature) {
$signature_data = $signature->getSignatureData();
$email = new PhutilEmailAddress($signature_data['email']);
$doc_name = $doc->getTitle();
$doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram());
$path = $this->getApplicationURI(sprintf(
'/verify/%s/',
$signature->getSecretKey()));
$link = PhabricatorEnv::getProductionURI($path);
$name = idx($signature_data, 'name');
$body = pht(
"%s:\n\n".
"This email address was used to sign a Legalpad document ".
"in Phabricator:\n\n".
" %s\n\n".
"Please verify you own this email address and accept the ".
"agreement by clicking this link:\n\n".
" %s\n\n".
"Your signature is not valid until you complete this ".
"verification step.\n\nYou can review the document here:\n\n".
" %s\n",
$name,
$doc_name,
$link,
$doc_link);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($email->getAddress()))
->setSubject(pht('[Legalpad] Signature Verification'))
->setForceDelivery(true)
->setBody($body)
->setRelatedPHID($signature->getDocumentPHID())
->saveAndSend();
}
private function signInResponse() {
return id(new Aphront403Response())
->setForbiddenText(
pht(
'The email address specified is associated with an account. '.
'Please login to that account and sign this document again.'));
}
}
diff --git a/src/applications/macro/engine/PhabricatorMemeEngine.php b/src/applications/macro/engine/PhabricatorMemeEngine.php
index 175aa2fb4..3e87a304c 100644
--- a/src/applications/macro/engine/PhabricatorMemeEngine.php
+++ b/src/applications/macro/engine/PhabricatorMemeEngine.php
@@ -1,390 +1,390 @@
<?php
final class PhabricatorMemeEngine extends Phobject {
private $viewer;
private $template;
private $aboveText;
private $belowText;
private $templateFile;
private $metrics;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setTemplate($template) {
$this->template = $template;
return $this;
}
public function getTemplate() {
return $this->template;
}
public function setAboveText($above_text) {
$this->aboveText = $above_text;
return $this;
}
public function getAboveText() {
return $this->aboveText;
}
public function setBelowText($below_text) {
$this->belowText = $below_text;
return $this;
}
public function getBelowText() {
return $this->belowText;
}
public function getGenerateURI() {
return id(new PhutilURI('/macro/meme/'))
->alter('macro', $this->getTemplate())
->alter('above', $this->getAboveText())
->alter('below', $this->getBelowText());
}
public function newAsset() {
$cache = $this->loadCachedFile();
if ($cache) {
return $cache;
}
$template = $this->loadTemplateFile();
if (!$template) {
throw new Exception(
pht(
'Template "%s" is not a valid template.',
$template));
}
$hash = $this->newTransformHash();
$asset = $this->newAssetFile($template);
$xfile = id(new PhabricatorTransformedFile())
->setOriginalPHID($template->getPHID())
->setTransformedPHID($asset->getPHID())
->setTransform($hash);
try {
$caught = null;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$xfile->save();
} catch (Exception $ex) {
$caught = $ex;
}
unset($unguarded);
if ($caught) {
throw $caught;
}
return $asset;
} catch (AphrontDuplicateKeyQueryException $ex) {
$xfile = $this->loadCachedFile();
if (!$xfile) {
throw $ex;
}
return $xfile;
}
}
private function newTransformHash() {
$properties = array(
'kind' => 'meme',
- 'above' => phutil_utf8_strtoupper($this->getAboveText()),
- 'below' => phutil_utf8_strtoupper($this->getBelowText()),
+ 'above' => $this->getAboveText(),
+ 'below' => $this->getBelowText(),
);
$properties = phutil_json_encode($properties);
return PhabricatorHash::digestForIndex($properties);
}
public function loadCachedFile() {
$viewer = $this->getViewer();
$template_file = $this->loadTemplateFile();
if (!$template_file) {
return null;
}
$hash = $this->newTransformHash();
$xform = id(new PhabricatorTransformedFile())->loadOneWhere(
'originalPHID = %s AND transform = %s',
$template_file->getPHID(),
$hash);
if (!$xform) {
return null;
}
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($xform->getTransformedPHID()))
->executeOne();
}
private function loadTemplateFile() {
if ($this->templateFile === null) {
$viewer = $this->getViewer();
$template = $this->getTemplate();
$macro = id(new PhabricatorMacroQuery())
->setViewer($viewer)
->withNames(array($template))
->needFiles(true)
->executeOne();
if (!$macro) {
return null;
}
$this->templateFile = $macro->getFile();
}
return $this->templateFile;
}
private function newAssetFile(PhabricatorFile $template) {
$data = $this->newAssetData($template);
return PhabricatorFile::newFromFileData(
$data,
array(
'name' => 'meme-'.$template->getName(),
'canCDN' => true,
// In modern code these can end up linked directly in email, so let
// them stick around for a while.
'ttl.relative' => phutil_units('30 days in seconds'),
));
}
private function newAssetData(PhabricatorFile $template) {
$template_data = $template->loadFileData();
$result = $this->newImagemagickAsset($template, $template_data);
if ($result) {
return $result;
}
return $this->newGDAsset($template, $template_data);
}
private function newImagemagickAsset(
PhabricatorFile $template,
$template_data) {
// We're only going to use Imagemagick on GIFs.
$mime_type = $template->getMimeType();
if ($mime_type != 'image/gif') {
return null;
}
// We're only going to use Imagemagick if it is actually available.
$available = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
if (!$available) {
return null;
}
// Test of the GIF is an animated GIF. If it's a flat GIF, we'll fall
// back to GD.
$input = new TempFile();
Filesystem::writeFile($input, $template_data);
list($err, $out) = exec_manual('convert %s info:', $input);
if ($err) {
return null;
}
$split = phutil_split_lines($out);
$frames = count($split);
if ($frames <= 1) {
return null;
}
// Split the frames apart, transform each frame, then merge them back
// together.
$output = new TempFile();
$future = new ExecFuture(
'convert %s -coalesce +adjoin %s_%s',
$input,
$input,
'%09d');
$future->setTimeout(10)->resolvex();
$output_files = array();
for ($ii = 0; $ii < $frames; $ii++) {
$frame_name = sprintf('%s_%09d', $input, $ii);
$output_name = sprintf('%s_%09d', $output, $ii);
$output_files[] = $output_name;
$frame_data = Filesystem::readFile($frame_name);
$memed_frame_data = $this->newGDAsset($template, $frame_data);
Filesystem::writeFile($output_name, $memed_frame_data);
}
$future = new ExecFuture(
'convert -dispose background -loop 0 %Ls %s',
$output_files,
$output);
$future->setTimeout(10)->resolvex();
return Filesystem::readFile($output);
}
private function newGDAsset(PhabricatorFile $template, $data) {
$img = imagecreatefromstring($data);
if (!$img) {
throw new Exception(
pht('Failed to imagecreatefromstring() image template data.'));
}
$dx = imagesx($img);
$dy = imagesy($img);
$metrics = $this->getMetrics($dx, $dy);
$font = $this->getFont();
$size = $metrics['size'];
$above = $this->getAboveText();
if (strlen($above)) {
$x = (int)floor(($dx - $metrics['text']['above']['width']) / 2);
$y = $metrics['text']['above']['height'] + 12;
$this->drawText($img, $font, $metrics['size'], $x, $y, $above);
}
$below = $this->getBelowText();
if (strlen($below)) {
$x = (int)floor(($dx - $metrics['text']['below']['width']) / 2);
$y = $dy - 12 - $metrics['text']['below']['descend'];
$this->drawText($img, $font, $metrics['size'], $x, $y, $below);
}
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
$img,
$template->getMimeType());
}
private function getFont() {
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
$font_root = $phabricator_root.'/resources/font/';
if (Filesystem::pathExists($font_root.'impact.ttf')) {
$font_path = $font_root.'impact.ttf';
} else {
$font_path = $font_root.'tuffy.ttf';
}
return $font_path;
}
private function getMetrics($dim_x, $dim_y) {
if ($this->metrics === null) {
$font = $this->getFont();
$font_max = 72;
$font_min = 5;
$margin_x = 16;
$margin_y = 16;
$last = null;
$cursor = floor(($font_max + $font_min) / 2);
$min = $font_min;
$max = $font_max;
$texts = array(
'above' => $this->getAboveText(),
'below' => $this->getBelowText(),
);
$metrics = null;
$best = null;
while (true) {
$all_fit = true;
$text_metrics = array();
foreach ($texts as $key => $text) {
$box = imagettfbbox($cursor, 0, $font, $text);
$height = abs($box[3] - $box[5]);
$width = abs($box[0] - $box[2]);
// This is the number of pixels below the baseline that the
// text extends, for example if it has a "y".
$descend = $box[3];
if (($height + $margin_y) > $dim_y) {
$all_fit = false;
break;
}
if (($width + $margin_x) > $dim_x) {
$all_fit = false;
break;
}
$text_metrics[$key]['width'] = $width;
$text_metrics[$key]['height'] = $height;
$text_metrics[$key]['descend'] = $descend;
}
if ($all_fit || $best === null) {
$best = $cursor;
$metrics = $text_metrics;
}
if ($all_fit) {
$min = $cursor;
} else {
$max = $cursor;
}
$last = $cursor;
$cursor = floor(($max + $min) / 2);
if ($cursor === $last) {
break;
}
}
$this->metrics = array(
'size' => $best,
'text' => $metrics,
);
}
return $this->metrics;
}
private function drawText($img, $font, $size, $x, $y, $text) {
$text_color = imagecolorallocate($img, 255, 255, 255);
$border_color = imagecolorallocate($img, 0, 0, 0);
$border = 2;
for ($xx = ($x - $border); $xx <= ($x + $border); $xx += $border) {
for ($yy = ($y - $border); $yy <= ($y + $border); $yy += $border) {
if (($xx === $x) && ($yy === $y)) {
continue;
}
imagettftext($img, $size, 0, $xx, $yy, $border_color, $font, $text);
}
}
imagettftext($img, $size, 0, $x, $y, $text_color, $font, $text);
}
}
diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
index e57b9b545..1ed409666 100644
--- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php
+++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
@@ -1,117 +1,112 @@
<?php
final class PhabricatorManiphestApplication extends PhabricatorApplication {
public function getName() {
return pht('Maniphest');
}
public function getShortDescription() {
return pht('Tasks and Bugs');
}
public function getBaseURI() {
return '/maniphest/';
}
public function getIcon() {
return 'fa-anchor';
}
public function getTitleGlyph() {
return "\xE2\x9A\x93";
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getApplicationOrder() {
return 0.110;
}
public function getFactObjectsForAnalysis() {
return array(
new ManiphestTask(),
);
}
public function getRemarkupRules() {
return array(
new ManiphestRemarkupRule(),
);
}
public function getRoutes() {
return array(
'/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController',
'/maniphest/' => array(
$this->getQueryRoutePattern() => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
$this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController',
'task/' => array(
$this->getEditRoutePattern('edit/')
=> 'ManiphestTaskEditController',
),
'subpriority/' => 'ManiphestSubpriorityController',
),
);
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send email to these addresses to create tasks. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
protected function getCustomCapabilities() {
return array(
ManiphestDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created tasks.'),
'template' => ManiphestTaskPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
ManiphestDefaultEditCapability::CAPABILITY => array(
'caption' => pht('Default edit policy for newly created tasks.'),
'template' => ManiphestTaskPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
),
- ManiphestEditStatusCapability::CAPABILITY => array(),
- ManiphestEditAssignCapability::CAPABILITY => array(),
- ManiphestEditPoliciesCapability::CAPABILITY => array(),
- ManiphestEditPriorityCapability::CAPABILITY => array(),
- ManiphestEditProjectsCapability::CAPABILITY => array(),
ManiphestBulkEditCapability::CAPABILITY => array(),
);
}
public function getMailCommandObjects() {
return array(
'task' => array(
'name' => pht('Email Commands: Tasks'),
'header' => pht('Interacting with Maniphest Tasks'),
'object' => new ManiphestTask(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'tasks in Maniphest. These commands work when creating new tasks '.
'via email and when replying to existing tasks.'),
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
ManiphestTaskPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/maniphest/capability/ManiphestEditAssignCapability.php b/src/applications/maniphest/capability/ManiphestEditAssignCapability.php
deleted file mode 100644
index 5da3adf55..000000000
--- a/src/applications/maniphest/capability/ManiphestEditAssignCapability.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-final class ManiphestEditAssignCapability extends PhabricatorPolicyCapability {
-
- const CAPABILITY = 'maniphest.edit.assign';
-
- public function getCapabilityName() {
- return pht('Can Assign Tasks');
- }
-
- public function describeCapabilityRejection() {
- return pht('You do not have permission to assign tasks.');
- }
-
-}
diff --git a/src/applications/maniphest/capability/ManiphestEditPoliciesCapability.php b/src/applications/maniphest/capability/ManiphestEditPoliciesCapability.php
deleted file mode 100644
index 55dd37a91..000000000
--- a/src/applications/maniphest/capability/ManiphestEditPoliciesCapability.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-final class ManiphestEditPoliciesCapability
- extends PhabricatorPolicyCapability {
-
- const CAPABILITY = 'maniphest.edit.policies';
-
- public function getCapabilityName() {
- return pht('Can Edit Task Policies');
- }
-
- public function describeCapabilityRejection() {
- return pht('You do not have permission to edit task policies.');
- }
-
-}
diff --git a/src/applications/maniphest/capability/ManiphestEditPriorityCapability.php b/src/applications/maniphest/capability/ManiphestEditPriorityCapability.php
deleted file mode 100644
index 09da656f4..000000000
--- a/src/applications/maniphest/capability/ManiphestEditPriorityCapability.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-final class ManiphestEditPriorityCapability
- extends PhabricatorPolicyCapability {
-
- const CAPABILITY = 'maniphest.edit.priority';
-
- public function getCapabilityName() {
- return pht('Can Prioritize Tasks');
- }
-
- public function describeCapabilityRejection() {
- return pht('You do not have permission to prioritize tasks.');
- }
-
-}
diff --git a/src/applications/maniphest/capability/ManiphestEditProjectsCapability.php b/src/applications/maniphest/capability/ManiphestEditProjectsCapability.php
deleted file mode 100644
index 46367ae24..000000000
--- a/src/applications/maniphest/capability/ManiphestEditProjectsCapability.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-final class ManiphestEditProjectsCapability
- extends PhabricatorPolicyCapability {
-
- const CAPABILITY = 'maniphest.edit.projects';
-
- public function getCapabilityName() {
- return pht('Can Edit Task Projects');
- }
-
- public function describeCapabilityRejection() {
- return pht('You do not have permission to edit task projects.');
- }
-
-}
diff --git a/src/applications/maniphest/capability/ManiphestEditStatusCapability.php b/src/applications/maniphest/capability/ManiphestEditStatusCapability.php
deleted file mode 100644
index 171c2c77c..000000000
--- a/src/applications/maniphest/capability/ManiphestEditStatusCapability.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-final class ManiphestEditStatusCapability extends PhabricatorPolicyCapability {
-
- const CAPABILITY = 'maniphest.edit.status';
-
- public function getCapabilityName() {
- return pht('Can Edit Task Status');
- }
-
- public function describeCapabilityRejection() {
- return pht('You do not have permission to edit task status.');
- }
-
-}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 66247ca6d..7c77e4ace 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1059 +1,1014 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $moreValidationErrors = array();
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COLUMNS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this task.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return (bool)$new;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($xaction->getNewValue() as $move) {
$this->applyBoardMove($object, $move);
}
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$parent_xaction = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskUnblockTransaction::TRANSACTIONTYPE)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
if ($this->getIsNewObject()) {
$parent_xaction->setMetadataValue('blocker.new', true);
}
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht("One of a task's subtasks changes status."),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_columns = PhabricatorTransactions::TYPE_COLUMNS;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_columns) {
$moves = $xaction->getNewValue();
foreach ($moves as $move) {
$board_phids[] = $move['boardPHID'];
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
- protected function requireCapabilities(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- parent::requireCapabilities($object, $xaction);
-
- $app_capability_map = array(
- ManiphestTaskPriorityTransaction::TRANSACTIONTYPE =>
- ManiphestEditPriorityCapability::CAPABILITY,
- ManiphestTaskStatusTransaction::TRANSACTIONTYPE =>
- ManiphestEditStatusCapability::CAPABILITY,
- ManiphestTaskOwnerTransaction::TRANSACTIONTYPE =>
- ManiphestEditAssignCapability::CAPABILITY,
- PhabricatorTransactions::TYPE_EDIT_POLICY =>
- ManiphestEditPoliciesCapability::CAPABILITY,
- PhabricatorTransactions::TYPE_VIEW_POLICY =>
- ManiphestEditPoliciesCapability::CAPABILITY,
- );
-
-
- $transaction_type = $xaction->getTransactionType();
-
- $app_capability = null;
- if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
- switch ($xaction->getMetadataValue('edge:type')) {
- case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
- $app_capability = ManiphestEditProjectsCapability::CAPABILITY;
- break;
- }
- } else {
- $app_capability = idx($app_capability_map, $transaction_type);
- }
-
- if ($app_capability) {
- $app = id(new PhabricatorApplicationQuery())
- ->setViewer($this->getActor())
- ->withClasses(array('PhabricatorManiphestApplication'))
- ->executeOne();
- PhabricatorPolicyFilter::requireCapability(
- $this->getActor(),
- $app,
- $app_capability);
- }
- }
-
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
continue;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 1.0;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to spread out all
// the nearby tasks.
$adjacent_sub = $adjacent->getSubpriority();
if ((abs($adjacent_sub - $base) < $epsilon)) {
$base = self::disperseBlock(
$dst,
$epsilon * 2);
if ($is_after) {
$sub = $base - $epsilon;
} else {
$sub = $base + $epsilon;
}
} else {
$sub = ($adjacent_sub + $base) / 2;
}
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
/**
* Distribute a cluster of tasks with similar subpriorities.
*/
private static function disperseBlock(
ManiphestTask $task,
$spacing) {
$conn = $task->establishConnection('w');
// Find a block of subpriority space which is, on average, sparse enough
// to hold all the tasks that are inside it with a reasonable level of
// separation between them.
// We'll start by looking near the target task for a range of numbers
// which has more space available than tasks. For example, if the target
// task has subpriority 33 and we want to separate each task by at least 1,
// we might start by looking in the range [23, 43].
// If we find fewer than 20 tasks there, we have room to reassign them
// with the desired level of separation. We space them out, then we're
// done.
// However: if we find more than 20 tasks, we don't have enough room to
// distribute them. We'll widen our search and look in a bigger range,
// maybe [13, 53]. This range has more space, so if we find fewer than
// 40 tasks in this range we can spread them out. If we still find too
// many tasks, we keep widening the search.
$base = $task->getSubpriority();
$scale = 4.0;
while (true) {
$range = ($spacing * $scale) / 2.0;
$min = ($base - $range);
$max = ($base + $range);
$result = queryfx_one(
$conn,
'SELECT COUNT(*) N FROM %T WHERE priority = %d AND
subpriority BETWEEN %f AND %f',
$task->getTableName(),
$task->getPriority(),
$min,
$max);
$count = $result['N'];
if ($count < $scale) {
// We have found a block which we can make sparse enough, so bail and
// continue below with our selection.
break;
}
// This block had too many tasks for its size, so try again with a
// bigger block.
$scale *= 2.0;
}
$rows = queryfx_all(
$conn,
'SELECT id FROM %T WHERE priority = %d AND
subpriority BETWEEN %f AND %f
ORDER BY priority, subpriority, id',
$task->getTableName(),
$task->getPriority(),
$min,
$max);
$task_id = $task->getID();
$result = null;
// NOTE: In strict mode (which we encourage enabling) we can't structure
// this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we
// provide default values for ALL of the columns that don't have defaults.
// This is gross, but we may be moving enough rows that individual
// queries are unreasonably slow. An alternate construction which might
// be worth evaluating is to use "CASE". Another approach is to disable
// strict mode for this query.
$extra_columns = array(
'phid' => '""',
'authorPHID' => '""',
'status' => '""',
'priority' => 0,
'title' => '""',
'description' => '""',
'dateCreated' => 0,
'dateModified' => 0,
'mailKey' => '""',
'viewPolicy' => '""',
'editPolicy' => '""',
'ownerOrdering' => '""',
'spacePHID' => '""',
'bridgedObjectPHID' => '""',
'properties' => '""',
'points' => 0,
'subtype' => '""',
);
$defaults = implode(', ', $extra_columns);
$sql = array();
$offset = 0;
// Often, we'll have more room than we need in the range. Distribute the
// tasks evenly over the whole range so that we're less likely to end up
// with tasks spaced exactly the minimum distance apart, which may
// get shifted again later. We have one fewer space to distribute than we
// have tasks.
$divisor = (double)(count($rows) - 1.0);
if ($divisor > 0) {
$available_distance = (($max - $min) / $divisor);
} else {
$available_distance = 0.0;
}
foreach ($rows as $row) {
$subpriority = $min + ($offset * $available_distance);
// If this is the task that we're spreading out relative to, keep track
// of where it is ending up so we can return the new subpriority.
$id = $row['id'];
if ($id == $task_id) {
$result = $subpriority;
}
$sql[] = qsprintf(
$conn,
'(%d, %Q, %f)',
$id,
$defaults,
$subpriority);
$offset++;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (id, %Q, subpriority) VALUES %Q
ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)',
$task->getTableName(),
implode(', ', array_keys($extra_columns)),
$chunk);
}
return $result;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = parent::validateAllTransactions($object, $xactions);
if ($this->moreValidationErrors) {
$errors = array_merge($errors, $this->moreValidationErrors);
}
return $errors;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_unassigned = ($object->getOwnerPHID() === null);
$any_assign = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() ==
ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {
$any_assign = true;
break;
}
}
$is_open = !$object->isClosed();
$new_status = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$new_status = $xaction->getNewValue();
break;
}
}
if ($new_status === null) {
$is_closing = false;
} else {
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
}
// If the task is not assigned, not being assigned, currently open, and
// being closed, try to assign the actor as the owner.
if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
// Don't assign the actor if they aren't a real user.
// Don't claim the task if the status is configured to not claim.
if ($actor_phid && $is_claim) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setNewValue($actor_phid);
}
}
// Automatically subscribe the author when they create a task.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COLUMNS:
try {
$more_xactions = $this->buildMoveTransaction($object, $xaction);
foreach ($more_xactions as $more_xaction) {
$results[] = $more_xaction;
}
} catch (Exception $ex) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
$this->moreValidationErrors[] = $error;
}
break;
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
// If this is a no-op update, don't expand it.
$old_value = $object->getOwnerPHID();
$new_value = $xaction->getNewValue();
if ($old_value === $new_value) {
continue;
}
// When a task is reassigned, move the old owner to the subscriber
// list so they're still in the loop.
if ($old_value) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array($old_value => $old_value),
));
}
break;
}
return $results;
}
private function buildMoveTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
if (!is_array($new)) {
$this->validateColumnPHID($new);
$new = array($new);
}
$nearby_phids = array();
foreach ($new as $key => $value) {
if (!is_array($value)) {
$this->validateColumnPHID($value);
$value = array(
'columnPHID' => $value,
);
}
PhutilTypeSpec::checkMap(
$value,
array(
'columnPHID' => 'string',
'beforePHID' => 'optional string',
'afterPHID' => 'optional string',
));
$new[$key] = $value;
if (!empty($value['beforePHID'])) {
$nearby_phids[] = $value['beforePHID'];
}
if (!empty($value['afterPHID'])) {
$nearby_phids[] = $value['afterPHID'];
}
}
if ($nearby_phids) {
$nearby_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($nearby_phids)
->execute();
$nearby_objects = mpull($nearby_objects, null, 'getPHID');
} else {
$nearby_objects = array();
}
$column_phids = ipull($new, 'columnPHID');
if ($column_phids) {
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($this->getActor())
->withPHIDs($column_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
} else {
$columns = array();
}
$board_phids = mpull($columns, 'getProjectPHID');
$object_phid = $object->getPHID();
$object_phids = $nearby_phids;
// Note that we may not have an object PHID if we're creating a new
// object.
if ($object_phid) {
$object_phids[] = $object_phid;
}
if ($object_phids) {
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($this->getActor())
->setBoardPHIDs($board_phids)
->setObjectPHIDs($object_phids)
->setFetchAllBoards(true)
->executeLayout();
}
foreach ($new as $key => $spec) {
$column_phid = $spec['columnPHID'];
$column = idx($columns, $column_phid);
if (!$column) {
throw new Exception(
pht(
'Column move transaction specifies column PHID "%s", but there '.
'is no corresponding column with this PHID.',
$column_phid));
}
$board_phid = $column->getProjectPHID();
$nearby = array();
if (!empty($spec['beforePHID'])) {
$nearby['beforePHID'] = $spec['beforePHID'];
}
if (!empty($spec['afterPHID'])) {
$nearby['afterPHID'] = $spec['afterPHID'];
}
if (count($nearby) > 1) {
throw new Exception(
pht(
'Column move transaction moves object to multiple positions. '.
'Specify only "beforePHID" or "afterPHID", not both.'));
}
foreach ($nearby as $where => $nearby_phid) {
if (empty($nearby_objects[$nearby_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s", but '.
'there is no corresponding object with this PHID.',
$object_phid,
$where));
}
$nearby_columns = $layout_engine->getObjectColumns(
$board_phid,
$nearby_phid);
$nearby_columns = mpull($nearby_columns, null, 'getPHID');
if (empty($nearby_columns[$column_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s" in '.
'column "%s", but this object is not in that column!',
$nearby_phid,
$where,
$column_phid));
}
}
if ($object_phid) {
$old_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$old_column_phids = mpull($old_columns, 'getPHID');
} else {
$old_column_phids = array();
}
$spec += array(
'boardPHID' => $board_phid,
'fromColumnPHIDs' => $old_column_phids,
);
// Check if the object is already in this column, and isn't being moved.
// We can just drop this column change if it has no effect.
$from_map = array_fuse($spec['fromColumnPHIDs']);
$already_here = isset($from_map[$column_phid]);
$is_reordering = (bool)$nearby;
if ($already_here && !$is_reordering) {
unset($new[$key]);
} else {
$new[$key] = $spec;
}
}
$new = array_values($new);
$xaction->setNewValue($new);
$more = array();
// If we're moving the object into a column and it does not already belong
// in the column, add the appropriate board. For normal columns, this
// is the board PHID. For proxy columns, it is the proxy PHID, unless the
// object is already a member of some descendant of the proxy PHID.
// The major case where this can happen is moves via the API, but it also
// happens when a user drags a task from the "Backlog" to a milestone
// column.
if ($object_phid) {
$current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object_phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$current_phids = array_fuse($current_phids);
} else {
$current_phids = array();
}
$add_boards = array();
foreach ($new as $move) {
$column_phid = $move['columnPHID'];
$board_phid = $move['boardPHID'];
$column = $columns[$column_phid];
$proxy_phid = $column->getProxyPHID();
// If this is a normal column, add the board if the object isn't already
// associated.
if (!$proxy_phid) {
if (!isset($current_phids[$board_phid])) {
$add_boards[] = $board_phid;
}
continue;
}
// If this is a proxy column but the object is already associated with
// the proxy board, we don't need to do anything.
if (isset($current_phids[$proxy_phid])) {
continue;
}
// If this a proxy column and the object is already associated with some
// descendant of the proxy board, we also don't need to do anything.
$descendants = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAncestorProjectPHIDs(array($proxy_phid))
->execute();
$found_descendant = false;
foreach ($descendants as $descendant) {
if (isset($current_phids[$descendant->getPHID()])) {
$found_descendant = true;
break;
}
}
if ($found_descendant) {
continue;
}
// Otherwise, we're moving the object to a proxy column which it is not
// a member of yet, so add an association to the column's proxy board.
$add_boards[] = $proxy_phid;
}
if ($add_boards) {
$more[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array_fuse($add_boards),
));
}
return $more;
}
private function applyBoardMove($object, array $move) {
$board_phid = $move['boardPHID'];
$column_phid = $move['columnPHID'];
$before_phid = idx($move, 'beforePHID');
$after_phid = idx($move, 'afterPHID');
$object_phid = $object->getPHID();
// We're doing layout with the omnipotent viewer to make sure we don't
// remove positions in columns that exist, but which the actual actor
// can't see.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$select_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($omnipotent_viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
$board_tasks = id(new ManiphestTaskQuery())
->setViewer($omnipotent_viewer)
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->execute();
$board_tasks = mpull($board_tasks, null, 'getPHID');
$board_tasks[$object_phid] = $object;
// Make sure tasks are sorted by ID, so we lay out new positions in
// a consistent way.
$board_tasks = msort($board_tasks, 'getID');
$object_phids = array_keys($board_tasks);
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($omnipotent_viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($object_phids)
->executeLayout();
// TODO: This logic needs to be revised when we legitimately support
// multiple column positions.
$columns = $engine->getObjectColumns($board_phid, $object_phid);
foreach ($columns as $column) {
$engine->queueRemovePosition(
$board_phid,
$column->getPHID(),
$object_phid);
}
if ($before_phid) {
$engine->queueAddPositionBefore(
$board_phid,
$column_phid,
$object_phid,
$before_phid);
} else if ($after_phid) {
$engine->queueAddPositionAfter(
$board_phid,
$column_phid,
$object_phid,
$after_phid);
} else {
$engine->queueAddPosition(
$board_phid,
$column_phid,
$object_phid);
}
$engine->applyPositionUpdates();
}
private function validateColumnPHID($value) {
if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
return;
}
throw new Exception(
pht(
'When moving objects between columns on a board, columns must '.
'be identified by PHIDs. This transaction uses "%s" to identify '.
'a column, but that is not a valid column PHID.',
$value));
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index edb780b72..ccf93c4a2 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,987 +1,995 @@
<?php
/**
* Query tasks by specific criteria. This class uses the higher-performance
* but less-general Maniphest indexes to satisfy queries.
*/
final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $taskIDs;
private $taskPHIDs;
private $authorPHIDs;
private $ownerPHIDs;
private $noOwner;
private $anyOwner;
private $subscriberPHIDs;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $dateModifiedAfter;
private $dateModifiedBefore;
private $bridgedObjectPHIDs;
private $hasOpenParents;
private $hasOpenSubtasks;
private $parentTaskIDs;
private $subtaskIDs;
private $subtypes;
private $closedEpochMin;
private $closedEpochMax;
private $closerPHIDs;
private $columnPHIDs;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_RESOLVED = 'status-resolved';
const STATUS_WONTFIX = 'status-wontfix';
const STATUS_INVALID = 'status-invalid';
const STATUS_SPITE = 'status-spite';
const STATUS_DUPLICATE = 'status-duplicate';
private $statuses;
private $priorities;
private $subpriorities;
private $groupBy = 'group-none';
const GROUP_NONE = 'group-none';
const GROUP_PRIORITY = 'group-priority';
const GROUP_OWNER = 'group-owner';
const GROUP_STATUS = 'group-status';
const GROUP_PROJECT = 'group-project';
const ORDER_PRIORITY = 'order-priority';
const ORDER_CREATED = 'order-created';
const ORDER_MODIFIED = 'order-modified';
const ORDER_TITLE = 'order-title';
private $needSubscriberPHIDs;
private $needProjectPHIDs;
public function withAuthors(array $authors) {
$this->authorPHIDs = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->taskPHIDs = $phids;
return $this;
}
public function withOwners(array $owners) {
+ if ($owners === array()) {
+ throw new Exception(pht('Empty withOwners() constraint is not valid.'));
+ }
+
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
foreach ($owners as $k => $phid) {
if ($phid === $no_owner || $phid === null) {
$this->noOwner = true;
unset($owners[$k]);
break;
}
if ($phid === $any_owner) {
$this->anyOwner = true;
unset($owners[$k]);
break;
}
}
- $this->ownerPHIDs = $owners;
+
+ if ($owners) {
+ $this->ownerPHIDs = $owners;
+ }
+
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withPriorities(array $priorities) {
$this->priorities = $priorities;
return $this;
}
public function withSubpriorities(array $subpriorities) {
$this->subpriorities = $subpriorities;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
switch ($this->groupBy) {
case self::GROUP_NONE:
$vector = array();
break;
case self::GROUP_PRIORITY:
$vector = array('priority');
break;
case self::GROUP_OWNER:
$vector = array('owner');
break;
case self::GROUP_STATUS:
$vector = array('status');
break;
case self::GROUP_PROJECT:
$vector = array('project');
break;
}
$this->setGroupVector($vector);
return $this;
}
public function withOpenSubtasks($value) {
$this->hasOpenSubtasks = $value;
return $this;
}
public function withOpenParents($value) {
$this->hasOpenParents = $value;
return $this;
}
public function withParentTaskIDs(array $ids) {
$this->parentTaskIDs = $ids;
return $this;
}
public function withSubtaskIDs(array $ids) {
$this->subtaskIDs = $ids;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withDateModifiedBefore($date_modified_before) {
$this->dateModifiedBefore = $date_modified_before;
return $this;
}
public function withDateModifiedAfter($date_modified_after) {
$this->dateModifiedAfter = $date_modified_after;
return $this;
}
public function withClosedEpochBetween($min, $max) {
$this->closedEpochMin = $min;
$this->closedEpochMax = $max;
return $this;
}
public function withCloserPHIDs(array $phids) {
$this->closerPHIDs = $phids;
return $this;
}
public function needSubscriberPHIDs($bool) {
$this->needSubscriberPHIDs = $bool;
return $this;
}
public function needProjectPHIDs($bool) {
$this->needProjectPHIDs = $bool;
return $this;
}
public function withBridgedObjectPHIDs(array $phids) {
$this->bridgedObjectPHIDs = $phids;
return $this;
}
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function withColumnPHIDs(array $column_phids) {
$this->columnPHIDs = $column_phids;
return $this;
}
public function newResultObject() {
return new ManiphestTask();
}
protected function loadPage() {
$task_dao = new ManiphestTask();
$conn = $task_dao->establishConnection('r');
$where = $this->buildWhereClause($conn);
$group_column = '';
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$group_column = qsprintf(
$conn,
', projectGroupName.indexedObjectPHID projectGroupPHID');
break;
}
$rows = queryfx_all(
$conn,
'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$group_column,
$task_dao->getTableName(),
$this->buildJoinClause($conn),
$where,
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$data = ipull($rows, null, 'id');
break;
default:
$data = $rows;
break;
}
$data = $this->didLoadRawRows($data);
$tasks = $task_dao->loadAllFromArray($data);
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$results = array();
foreach ($rows as $row) {
$task = clone $tasks[$row['id']];
$task->attachGroupByProjectPHID($row['projectGroupPHID']);
$results[] = $task;
}
$tasks = $results;
break;
}
return $tasks;
}
protected function willFilterPage(array $tasks) {
if ($this->groupBy == self::GROUP_PROJECT) {
// We should only return project groups which the user can actually see.
$project_phids = mpull($tasks, 'getGroupByProjectPHID');
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($tasks as $key => $task) {
if (!$task->getGroupByProjectPHID()) {
// This task is either not tagged with any projects, or only tagged
// with projects which we're ignoring because they're being queried
// for explicitly.
continue;
}
if (empty($projects[$task->getGroupByProjectPHID()])) {
unset($tasks[$key]);
}
}
}
return $tasks;
}
protected function didFilterPage(array $tasks) {
$phids = mpull($tasks, 'getPHID');
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($tasks as $task) {
$project_phids = $edge_query->getDestinationPHIDs(
array($task->getPHID()));
$task->attachProjectPHIDs($project_phids);
}
}
if ($this->needSubscriberPHIDs) {
$subscriber_sets = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs($phids)
->execute();
foreach ($tasks as $task) {
$subscribers = idx($subscriber_sets, $task->getPHID(), array());
$task->attachSubscriberPHIDs($subscribers);
}
}
return $tasks;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
if ($this->taskIDs !== null) {
$where[] = qsprintf(
$conn,
'task.id in (%Ld)',
$this->taskIDs);
}
if ($this->taskPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.phid in (%Ls)',
$this->taskPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'task.status IN (%Ls)',
$this->statuses);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.authorPHID in (%Ls)',
$this->authorPHIDs);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'task.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'task.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->dateModifiedAfter) {
$where[] = qsprintf(
$conn,
'task.dateModified >= %d',
$this->dateModifiedAfter);
}
if ($this->dateModifiedBefore) {
$where[] = qsprintf(
$conn,
'task.dateModified <= %d',
$this->dateModifiedBefore);
}
if ($this->closedEpochMin !== null) {
$where[] = qsprintf(
$conn,
'task.closedEpoch >= %d',
$this->closedEpochMin);
}
if ($this->closedEpochMax !== null) {
$where[] = qsprintf(
$conn,
'task.closedEpoch <= %d',
$this->closedEpochMax);
}
if ($this->closerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.closerPHID IN (%Ls)',
$this->closerPHIDs);
}
if ($this->priorities !== null) {
$where[] = qsprintf(
$conn,
'task.priority IN (%Ld)',
$this->priorities);
}
if ($this->subpriorities !== null) {
$where[] = qsprintf(
$conn,
'task.subpriority IN (%Lf)',
$this->subpriorities);
}
if ($this->bridgedObjectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.bridgedObjectPHID IN (%Ls)',
$this->bridgedObjectPHIDs);
}
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'task.subtype IN (%Ls)',
$this->subtypes);
}
if ($this->columnPHIDs !== null) {
$viewer = $this->getViewer();
$columns = id(new PhabricatorProjectColumnQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($this->columnPHIDs)
->execute();
if (!$columns) {
throw new PhabricatorEmptyQueryException();
}
// We must do board layout before we move forward because the column
// positions may not yet exist otherwise. An example is that newly
// created tasks may not yet be positioned in the backlog column.
$projects = mpull($columns, 'getProject');
$projects = mpull($projects, null, 'getPHID');
// The board layout engine needs to know about every object that it's
// going to be asked to do layout for. For now, we're just doing layout
// on every object on the boards. In the future, we could do layout on a
// smaller set of objects by using the constraints on this Query. For
// example, if the caller is only asking for open tasks, we only need
// to do layout on open tasks.
// This fetches too many objects (every type of object tagged with the
// project, not just tasks). We could narrow it by querying the edge
// table on the Maniphest side, but there's currently no way to build
// that query with EdgeQuery.
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array_keys($projects))
->withEdgeTypes(
array(
PhabricatorProjectProjectHasObjectEdgeType::EDGECONST,
));
$edge_query->execute();
$all_phids = $edge_query->getDestinationPHIDs();
// Since we overfetched PHIDs, filter out any non-tasks we got back.
foreach ($all_phids as $key => $phid) {
if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) {
unset($all_phids[$key]);
}
}
// If there are no tasks on the relevant boards, this query can't
// possibly hit anything so we're all done.
$task_phids = array_fuse($all_phids);
if (!$task_phids) {
throw new PhabricatorEmptyQueryException();
}
// We know everything we need to know, so perform board layout.
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setFetchAllBoards(true)
->setBoardPHIDs(array_keys($projects))
->setObjectPHIDs($task_phids)
->executeLayout();
// Find the tasks that are in the constraint columns after board layout
// completes.
$select_phids = array();
foreach ($columns as $column) {
$in_column = $engine->getColumnObjectPHIDs(
$column->getProjectPHID(),
$column->getPHID());
foreach ($in_column as $phid) {
$select_phids[$phid] = $phid;
}
}
if (!$select_phids) {
throw new PhabricatorEmptyQueryException();
}
$where[] = qsprintf(
$conn,
'task.phid IN (%Ls)',
$select_phids);
}
return $where;
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
case self::STATUS_CLOSED:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getClosedStatusConstants());
default:
$constant = idx($map, $this->status);
if (!$constant) {
throw new Exception(pht("Unknown status query '%s'!", $this->status));
}
return qsprintf(
$conn,
'task.status = %s',
$constant);
}
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
$subclause = array();
if ($this->noOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NULL');
}
if ($this->anyOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NOT NULL');
}
- if ($this->ownerPHIDs) {
+ if ($this->ownerPHIDs !== null) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if (!$subclause) {
return '';
}
return '('.implode(') OR (', $subclause).')';
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$open_statuses = ManiphestTaskStatus::getOpenStatusConstants();
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$task_table = $this->newResultObject()->getTableName();
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$joins = array();
if ($this->hasOpenParents !== null) {
if ($this->hasOpenParents) {
$join_type = 'JOIN';
} else {
$join_type = 'LEFT JOIN';
}
$joins[] = qsprintf(
$conn,
'%Q %T e_parent
ON e_parent.src = task.phid
AND e_parent.type = %d
%Q %T parent
ON e_parent.dst = parent.phid
AND parent.status IN (%Ls)',
$join_type,
$edge_table,
$parent_type,
$join_type,
$task_table,
$open_statuses);
}
if ($this->hasOpenSubtasks !== null) {
if ($this->hasOpenSubtasks) {
$join_type = 'JOIN';
} else {
$join_type = 'LEFT JOIN';
}
$joins[] = qsprintf(
$conn,
'%Q %T e_subtask
ON e_subtask.src = task.phid
AND e_subtask.type = %d
%Q %T subtask
ON e_subtask.dst = subtask.phid
AND subtask.status IN (%Ls)',
$join_type,
$edge_table,
$subtask_type,
$join_type,
$task_table,
$open_statuses);
}
if ($this->subscriberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_ccs ON e_ccs.src = task.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->subscriberPHIDs);
}
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
if ($ignore_group_phids) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d
AND projectGroup.dst NOT IN (%Ls)',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$ignore_group_phids);
} else {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
}
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T projectGroupName
ON projectGroup.dst = projectGroupName.indexedObjectPHID',
id(new ManiphestNameIndex())->getTableName());
break;
}
if ($this->parentTaskIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_has_parent
ON e_has_parent.src = task.phid
AND e_has_parent.type = %d
JOIN %T has_parent
ON e_has_parent.dst = has_parent.phid
AND has_parent.id IN (%Ld)',
$edge_table,
$parent_type,
$task_table,
$this->parentTaskIDs);
}
if ($this->subtaskIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_has_subtask
ON e_has_subtask.src = task.phid
AND e_has_subtask.type = %d
JOIN %T has_subtask
ON e_has_subtask.dst = has_subtask.phid
AND has_subtask.id IN (%Ld)',
$edge_table,
$subtask_type,
$task_table,
$this->subtaskIDs);
}
$joins[] = parent::buildJoinClauseParts($conn);
return $joins;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
$joined_multiple_rows =
($this->hasOpenParents !== null) ||
($this->hasOpenSubtasks !== null) ||
($this->parentTaskIDs !== null) ||
($this->subtaskIDs !== null) ||
$this->shouldGroupQueryResultRows();
$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
// If we're joining multiple rows, we need to group the results by the
// task IDs.
if ($joined_multiple_rows) {
if ($joined_project_name) {
return 'GROUP BY task.phid, projectGroup.dst';
} else {
return 'GROUP BY task.phid';
}
} else {
return '';
}
}
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = parent::buildHavingClauseParts($conn);
if ($this->hasOpenParents !== null) {
if (!$this->hasOpenParents) {
$having[] = qsprintf(
$conn,
'COUNT(parent.phid) = 0');
}
}
if ($this->hasOpenSubtasks !== null) {
if (!$this->hasOpenSubtasks) {
$having[] = qsprintf(
$conn,
'COUNT(subtask.phid) = 0');
}
}
return $having;
}
/**
* Return project PHIDs which we should ignore when grouping tasks by
* project. For example, if a user issues a query like:
*
* Tasks tagged with all projects: Frontend, Bugs
*
* ...then we don't show "Frontend" or "Bugs" groups in the result set, since
* they're meaningless as all results are in both groups.
*
* Similarly, for queries like:
*
* Tasks tagged with any projects: Public Relations
*
* ...we ignore the single project, as every result is in that project. (In
* the case that there are several "any" projects, we do not ignore them.)
*
* @return list<phid> Project PHIDs which should be ignored in query
* construction.
*/
private function getIgnoreGroupedProjectPHIDs() {
// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
// impact the results, but we might end up with a better query plan.
// Investigate this on real data? This is likely very rare.
$edge_types = array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
);
$phids = array();
$phids[] = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_AND,
));
$any = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_OR,
));
if (count($any) == 1) {
$phids[] = $any;
}
return array_mergev($phids);
}
protected function getResultCursor($result) {
$id = $result->getID();
if ($this->groupBy == self::GROUP_PROJECT) {
return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
}
return $id;
}
public function getBuiltinOrders() {
$orders = array(
'priority' => array(
'vector' => array('priority', 'subpriority', 'id'),
'name' => pht('Priority'),
'aliases' => array(self::ORDER_PRIORITY),
),
'updated' => array(
'vector' => array('updated', 'id'),
'name' => pht('Date Updated (Latest First)'),
'aliases' => array(self::ORDER_MODIFIED),
),
'outdated' => array(
'vector' => array('-updated', '-id'),
'name' => pht('Date Updated (Oldest First)'),
),
'closed' => array(
'vector' => array('closed', 'id'),
'name' => pht('Date Closed (Latest First)'),
),
'title' => array(
'vector' => array('title', 'id'),
'name' => pht('Title'),
'aliases' => array(self::ORDER_TITLE),
),
) + parent::getBuiltinOrders();
// Alias the "newest" builtin to the historical key for it.
$orders['newest']['aliases'][] = self::ORDER_CREATED;
$orders = array_select_keys(
$orders,
array(
'priority',
'updated',
'outdated',
'newest',
'oldest',
'closed',
'title',
)) + $orders;
return $orders;
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'priority' => array(
'table' => 'task',
'column' => 'priority',
'type' => 'int',
),
'owner' => array(
'table' => 'task',
'column' => 'ownerOrdering',
'null' => 'head',
'reverse' => true,
'type' => 'string',
),
'status' => array(
'table' => 'task',
'column' => 'status',
'type' => 'string',
'reverse' => true,
),
'project' => array(
'table' => 'projectGroupName',
'column' => 'indexedObjectName',
'type' => 'string',
'null' => 'head',
'reverse' => true,
),
'title' => array(
'table' => 'task',
'column' => 'title',
'type' => 'string',
'reverse' => true,
),
'subpriority' => array(
'table' => 'task',
'column' => 'subpriority',
'type' => 'float',
),
'updated' => array(
'table' => 'task',
'column' => 'dateModified',
'type' => 'int',
),
'closed' => array(
'table' => 'task',
'column' => 'closedEpoch',
'type' => 'int',
'null' => 'tail',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$cursor_parts = explode('.', $cursor, 2);
$task_id = $cursor_parts[0];
$group_id = idx($cursor_parts, 1);
$task = $this->loadCursorObject($task_id);
$map = array(
'id' => $task->getID(),
'priority' => $task->getPriority(),
'subpriority' => $task->getSubpriority(),
'owner' => $task->getOwnerOrdering(),
'status' => $task->getStatus(),
'title' => $task->getTitle(),
'updated' => $task->getDateModified(),
'closed' => $task->getClosedEpoch(),
);
foreach ($keys as $key) {
switch ($key) {
case 'project':
$value = null;
if ($group_id) {
$paging_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_id))
->execute();
if ($paging_projects) {
$value = head($paging_projects)->getName();
}
}
$map[$key] = $value;
break;
}
}
foreach ($keys as $key) {
if ($this->isCustomFieldOrderKey($key)) {
$map += $this->getPagingValueMapForCustomFields($task);
break;
}
}
return $map;
}
protected function getPrimaryTableAlias() {
return 'task';
}
public function getQueryApplicationClass() {
return 'PhabricatorManiphestApplication';
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index f85c79621..437362fa4 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -1,592 +1,588 @@
<?php
final class ManiphestTaskSearchEngine
extends PhabricatorApplicationSearchEngine {
private $showBatchControls;
private $baseURI;
private $isBoardView;
public function setIsBoardView($is_board_view) {
$this->isBoardView = $is_board_view;
return $this;
}
public function getIsBoardView() {
return $this->isBoardView;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function getResultTypeDescription() {
return pht('Maniphest Tasks');
}
public function getApplicationClassName() {
return 'PhabricatorManiphestApplication';
}
public function newQuery() {
return id(new ManiphestTaskQuery())
->needProjectPHIDs(true);
}
protected function buildCustomSearchFields() {
// Hide the "Subtypes" constraint from the web UI if the install only
// defines one task subtype, since it isn't of any use in this case.
$subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap();
$hide_subtypes = (count($subtype_map) == 1);
return array(
id(new PhabricatorOwnersSearchField())
->setLabel(pht('Assigned To'))
->setKey('assignedPHIDs')
->setConduitKey('assigned')
->setAliases(array('assigned'))
->setDescription(
pht('Search for tasks owned by a user from a list.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors'))
->setDescription(
pht('Search for tasks with given authors.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Statuses'))
->setKey('statuses')
->setAliases(array('status'))
->setDescription(
pht('Search for tasks with given statuses.'))
->setDatasource(new ManiphestTaskStatusFunctionDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Priorities'))
->setKey('priorities')
->setAliases(array('priority'))
->setDescription(
pht('Search for tasks with given priorities.'))
->setConduitParameterType(new ConduitIntListParameterType())
->setDatasource(new ManiphestTaskPriorityDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Subtypes'))
->setKey('subtypes')
->setAliases(array('subtype'))
->setDescription(
pht('Search for tasks with given subtypes.'))
->setDatasource(new ManiphestTaskSubtypeDatasource())
->setIsHidden($hide_subtypes),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Columns'))
->setKey('columnPHIDs')
->setAliases(array('column', 'columnPHID', 'columns')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Open Parents'))
->setKey('hasParents')
->setAliases(array('blocking'))
->setOptions(
pht('(Show All)'),
pht('Show Only Tasks With Open Parents'),
pht('Show Only Tasks Without Open Parents')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Open Subtasks'))
->setKey('hasSubtasks')
->setAliases(array('blocked'))
->setOptions(
pht('(Show All)'),
pht('Show Only Tasks With Open Subtasks'),
pht('Show Only Tasks Without Open Subtasks')),
id(new PhabricatorIDsSearchField())
->setLabel(pht('Parent IDs'))
->setKey('parentIDs')
->setAliases(array('parentID')),
id(new PhabricatorIDsSearchField())
->setLabel(pht('Subtask IDs'))
->setKey('subtaskIDs')
->setAliases(array('subtaskID')),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Group By'))
->setKey('group')
->setOptions($this->getGroupOptions()),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created After'))
->setKey('createdStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created Before'))
->setKey('createdEnd'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Updated After'))
->setKey('modifiedStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Updated Before'))
->setKey('modifiedEnd'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Closed After'))
->setKey('closedStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Closed Before'))
->setKey('closedEnd'),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Closed By'))
->setKey('closerPHIDs')
->setAliases(array('closer', 'closerPHID', 'closers'))
->setDescription(pht('Search for tasks closed by certain users.')),
id(new PhabricatorSearchTextField())
->setLabel(pht('Page Size'))
->setKey('limit'),
);
}
protected function getDefaultFieldOrder() {
return array(
'assignedPHIDs',
'projectPHIDs',
'authorPHIDs',
'subscriberPHIDs',
'statuses',
'priorities',
'subtypes',
'hasParents',
'hasSubtasks',
'parentIDs',
'subtaskIDs',
'group',
'order',
'ids',
'...',
'createdStart',
'createdEnd',
'modifiedStart',
'modifiedEnd',
'closedStart',
'closedEnd',
'closerPHIDs',
'limit',
);
}
protected function getHiddenFields() {
$keys = array();
if ($this->getIsBoardView()) {
$keys[] = 'group';
$keys[] = 'order';
$keys[] = 'limit';
}
return $keys;
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['assignedPHIDs']) {
$query->withOwners($map['assignedPHIDs']);
}
if ($map['authorPHIDs']) {
$query->withAuthors($map['authorPHIDs']);
}
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
if ($map['priorities']) {
$query->withPriorities($map['priorities']);
}
if ($map['subtypes']) {
$query->withSubtypes($map['subtypes']);
}
if ($map['createdStart']) {
$query->withDateCreatedAfter($map['createdStart']);
}
if ($map['createdEnd']) {
$query->withDateCreatedBefore($map['createdEnd']);
}
if ($map['modifiedStart']) {
$query->withDateModifiedAfter($map['modifiedStart']);
}
if ($map['modifiedEnd']) {
$query->withDateModifiedBefore($map['modifiedEnd']);
}
if ($map['closedStart'] || $map['closedEnd']) {
$query->withClosedEpochBetween($map['closedStart'], $map['closedEnd']);
}
if ($map['closerPHIDs']) {
$query->withCloserPHIDs($map['closerPHIDs']);
}
if ($map['hasParents'] !== null) {
$query->withOpenParents($map['hasParents']);
}
if ($map['hasSubtasks'] !== null) {
$query->withOpenSubtasks($map['hasSubtasks']);
}
if ($map['parentIDs']) {
$query->withParentTaskIDs($map['parentIDs']);
}
if ($map['subtaskIDs']) {
$query->withSubtaskIDs($map['subtaskIDs']);
}
if ($map['columnPHIDs']) {
$query->withColumnPHIDs($map['columnPHIDs']);
}
$group = idx($map, 'group');
$group = idx($this->getGroupValues(), $group);
if ($group) {
$query->setGroupBy($group);
}
if ($map['ids']) {
$ids = $map['ids'];
foreach ($ids as $key => $id) {
$id = trim($id, ' Tt');
if (!$id || !is_numeric($id)) {
unset($ids[$key]);
} else {
$ids[$key] = $id;
}
}
if ($ids) {
$query->withIDs($ids);
}
}
return $query;
}
protected function getURI($path) {
if ($this->baseURI) {
return $this->baseURI.$path;
}
return '/maniphest/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['assigned'] = pht('Assigned');
$names['authored'] = pht('Authored');
$names['subscribed'] = pht('Subscribed');
}
$names['open'] = pht('Open Tasks');
$names['all'] = pht('All Tasks');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'assigned':
return $query
->setParameter('assignedPHIDs', array($viewer_phid))
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'subscribed':
return $query
->setParameter('subscriberPHIDs', array($viewer_phid))
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'open':
return $query
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer_phid))
->setParameter('order', 'created')
->setParameter('group', 'none');
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getGroupOptions() {
return array(
'priority' => pht('Priority'),
'assigned' => pht('Assigned'),
'status' => pht('Status'),
'project' => pht('Project'),
'none' => pht('None'),
);
}
private function getGroupValues() {
return array(
'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
'assigned' => ManiphestTaskQuery::GROUP_OWNER,
'status' => ManiphestTaskQuery::GROUP_STATUS,
'project' => ManiphestTaskQuery::GROUP_PROJECT,
'none' => ManiphestTaskQuery::GROUP_NONE,
);
}
protected function renderResultList(
array $tasks,
PhabricatorSavedQuery $saved,
array $handles) {
$viewer = $this->requireViewer();
if ($this->isPanelContext()) {
$can_edit_priority = false;
$can_bulk_edit = false;
} else {
- $can_edit_priority = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $this->getApplication(),
- ManiphestEditPriorityCapability::CAPABILITY);
-
+ $can_edit_priority = true;
$can_bulk_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getApplication(),
ManiphestBulkEditCapability::CAPABILITY);
}
$list = id(new ManiphestTaskResultListView())
->setUser($viewer)
->setTasks($tasks)
->setSavedQuery($saved)
->setCanEditPriority($can_edit_priority)
->setCanBatchEdit($can_bulk_edit)
->setShowBatchControls($this->showBatchControls);
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($list);
return $result;
}
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
// The 'withUnassigned' parameter may be present in old saved queries from
// before parameterized typeaheads, and is retained for compatibility. We
// could remove it by migrating old saved queries.
$assigned_phids = $saved->getParameter('assignedPHIDs', array());
if ($saved->getParameter('withUnassigned')) {
$assigned_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
}
$saved->setParameter('assignedPHIDs', $assigned_phids);
// The 'projects' and other parameters may be present in old saved queries
// from before parameterized typeaheads.
$project_phids = $saved->getParameter('projectPHIDs', array());
$old = $saved->getParameter('projects', array());
foreach ($old as $phid) {
$project_phids[] = $phid;
}
$all = $saved->getParameter('allProjectPHIDs', array());
foreach ($all as $phid) {
$project_phids[] = $phid;
}
$any = $saved->getParameter('anyProjectPHIDs', array());
foreach ($any as $phid) {
$project_phids[] = 'any('.$phid.')';
}
$not = $saved->getParameter('excludeProjectPHIDs', array());
foreach ($not as $phid) {
$project_phids[] = 'not('.$phid.')';
}
$users = $saved->getParameter('userProjectPHIDs', array());
foreach ($users as $phid) {
$project_phids[] = 'projects('.$phid.')';
}
$no = $saved->getParameter('withNoProject');
if ($no) {
$project_phids[] = 'null()';
}
$saved->setParameter('projectPHIDs', $project_phids);
}
protected function getNewUserBody() {
$viewer = $this->requireViewer();
$create_button = id(new ManiphestEditEngine())
->setViewer($viewer)
->newNUXBUtton(pht('Create a Task'));
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('Use Maniphest to track bugs, features, todos, or anything else '.
'you need to get done. Tasks assigned to you will appear here.'))
->addAction($create_button);
return $view;
}
protected function newExportFields() {
$fields = array(
id(new PhabricatorStringExportField())
->setKey('monogram')
->setLabel(pht('Monogram')),
id(new PhabricatorPHIDExportField())
->setKey('authorPHID')
->setLabel(pht('Author PHID')),
id(new PhabricatorStringExportField())
->setKey('author')
->setLabel(pht('Author')),
id(new PhabricatorPHIDExportField())
->setKey('ownerPHID')
->setLabel(pht('Owner PHID')),
id(new PhabricatorStringExportField())
->setKey('owner')
->setLabel(pht('Owner')),
id(new PhabricatorStringExportField())
->setKey('status')
->setLabel(pht('Status')),
id(new PhabricatorStringExportField())
->setKey('statusName')
->setLabel(pht('Status Name')),
id(new PhabricatorEpochExportField())
->setKey('dateClosed')
->setLabel(pht('Date Closed')),
id(new PhabricatorPHIDExportField())
->setKey('closerPHID')
->setLabel(pht('Closer PHID')),
id(new PhabricatorStringExportField())
->setKey('closer')
->setLabel(pht('Closer')),
id(new PhabricatorStringExportField())
->setKey('priority')
->setLabel(pht('Priority')),
id(new PhabricatorStringExportField())
->setKey('priorityName')
->setLabel(pht('Priority Name')),
id(new PhabricatorStringExportField())
->setKey('subtype')
->setLabel('Subtype'),
id(new PhabricatorURIExportField())
->setKey('uri')
->setLabel(pht('URI')),
id(new PhabricatorStringExportField())
->setKey('title')
->setLabel(pht('Title')),
id(new PhabricatorStringExportField())
->setKey('description')
->setLabel(pht('Description')),
);
if (ManiphestTaskPoints::getIsEnabled()) {
- $fields[] = id(new PhabricatorIntExportField())
+ $fields[] = id(new PhabricatorDoubleExportField())
->setKey('points')
->setLabel('Points');
}
return $fields;
}
protected function newExportData(array $tasks) {
$viewer = $this->requireViewer();
$phids = array();
foreach ($tasks as $task) {
$phids[] = $task->getAuthorPHID();
$phids[] = $task->getOwnerPHID();
$phids[] = $task->getCloserPHID();
}
$handles = $viewer->loadHandles($phids);
$export = array();
foreach ($tasks as $task) {
$author_phid = $task->getAuthorPHID();
if ($author_phid) {
$author_name = $handles[$author_phid]->getName();
} else {
$author_name = null;
}
$owner_phid = $task->getOwnerPHID();
if ($owner_phid) {
$owner_name = $handles[$owner_phid]->getName();
} else {
$owner_name = null;
}
$closer_phid = $task->getCloserPHID();
if ($closer_phid) {
$closer_name = $handles[$closer_phid]->getName();
} else {
$closer_name = null;
}
$status_value = $task->getStatus();
$status_name = ManiphestTaskStatus::getTaskStatusName($status_value);
$priority_value = $task->getPriority();
$priority_name = ManiphestTaskPriority::getTaskPriorityName(
$priority_value);
$export[] = array(
'monogram' => $task->getMonogram(),
'authorPHID' => $author_phid,
'author' => $author_name,
'ownerPHID' => $owner_phid,
'owner' => $owner_name,
'status' => $status_value,
'statusName' => $status_name,
'priority' => $priority_value,
'priorityName' => $priority_name,
'points' => $task->getPoints(),
'subtype' => $task->getSubtype(),
'title' => $task->getTitle(),
'uri' => PhabricatorEnv::getProductionURI($task->getURI()),
'description' => $task->getDescription(),
'dateClosed' => $task->getClosedEpoch(),
'closerPHID' => $closer_phid,
'closer' => $closer_name,
);
}
return $export;
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php
index 83f38fc65..398808e00 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php
@@ -1,175 +1,204 @@
<?php
final class ManiphestTaskPriorityTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'priority';
public function generateOldValue($object) {
return (string)$object->getPriority();
}
public function generateNewValue($object, $value) {
// `$value` is supposed to be a keyword, but if the priority
// assigned to a task has been removed from the config,
// no such keyword will be available. Other edits to the task
// should still be allowed, even if the priority is no longer
// valid, so treat this as a no-op.
if ($value === ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD) {
return (string)$object->getPriority();
}
return (string)ManiphestTaskPriority::getTaskPriorityFromKeyword($value);
}
public function applyInternalEffects($object, $value) {
$object->setPriority($value);
}
public function getActionStrength() {
return 1.1;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return pht('Triaged');
} else if ($old > $new) {
return pht('Lowered Priority');
} else {
return pht('Raised Priority');
}
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return pht(
'%s triaged this task as %s priority.',
$this->renderAuthor(),
$this->renderValue($new_name));
} else if ($old > $new) {
return pht(
'%s lowered the priority of this task from %s to %s.',
$this->renderAuthor(),
$this->renderValue($old_name),
$this->renderValue($new_name));
} else {
return pht(
'%s raised the priority of this task from %s to %s.',
$this->renderAuthor(),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return pht(
'%s triaged %s as %s priority.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($new_name));
} else if ($old > $new) {
return pht(
'%s lowered the priority of %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($old_name),
$this->renderValue($new_name));
} else {
return pht(
'%s raised the priority of %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'fa-arrow-right';
} else if ($old > $new) {
return 'fa-arrow-down';
} else {
return 'fa-arrow-up';
}
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old == ManiphestTaskPriority::getDefaultPriority()) {
return 'green';
} else if ($old > $new) {
return 'grey';
} else {
return 'yellow';
}
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$content_source = $this->getEditor()->getContentSource();
$is_web = ($content_source instanceof PhabricatorWebContentSource);
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
// If this is a legitimate keyword like "low" or "high", this transaction
// is fine and apply normally.
$keyword = ManiphestTaskPriority::getTaskPriorityFromKeyword($value);
if ($keyword !== null) {
continue;
}
// If this is the magic "don't change things" value for editing tasks
// with an obsolete priority constant in the database, let it through if
// this is a web edit.
if ($value === ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD) {
if ($is_web) {
continue;
}
}
$keyword_list = array();
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $name) {
$keyword = ManiphestTaskPriority::getKeywordForTaskPriority($pri);
if ($keyword === null) {
continue;
}
$keyword_list[] = $keyword;
}
$errors[] = $this->newInvalidError(
pht(
'Task priority "%s" is not a valid task priority. Use a priority '.
'keyword to choose a task priority: %s.',
$value,
implode(', ', $keyword_list)),
$xaction);
}
return $errors;
}
+ public function getTransactionTypeForConduit($xaction) {
+ return 'priority';
+ }
+
+ public function getFieldValuesForConduit($xaction, $data) {
+ $old = $xaction->getOldValue();
+ if ($old !== null) {
+ $old = (int)$old;
+ $old_name = ManiphestTaskPriority::getTaskPriorityName($old);
+ } else {
+ $old_name = null;
+ }
+
+ $new = $xaction->getNewValue();
+ $new = (int)$new;
+ $new_name = ManiphestTaskPriority::getTaskPriorityName($new);
+
+ return array(
+ 'old' => array(
+ 'value' => $old,
+ 'name' => $old_name,
+ ),
+ 'new' => array(
+ 'value' => $new,
+ 'name' => $new_name,
+ ),
+ );
+ }
+
}
diff --git a/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php b/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
index 2979e9222..43ed2e957 100644
--- a/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
@@ -1,148 +1,148 @@
<?php
final class PhabricatorApplicationEmailCommandsController
extends PhabricatorApplicationsController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$application = $request->getURIData('application');
$selected = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array($application))
->executeOne();
if (!$selected) {
return new Aphront404Response();
}
$specs = $selected->getMailCommandObjects();
$type = $request->getURIData('type');
if (empty($specs[$type])) {
return new Aphront404Response();
}
$spec = $specs[$type];
$commands = MetaMTAEmailTransactionCommand::getAllCommandsForObject(
$spec['object']);
$commands = msort($commands, 'getCommand');
$content = array();
$content[] = '= '.pht('Mail Commands Overview');
$content[] = pht(
'After configuring Phabricator to process inbound mail, you can '.
'interact with objects (like tasks and revisions) over email. For '.
'information on configuring Phabricator, see '.
'**[[ %s | Configuring Inbound Email ]]**.'.
"\n\n".
'In most cases, you can reply to email you receive from Phabricator '.
'to leave comments. You can also use **mail commands** to take a '.
'greater range of actions (like claiming a task or requesting changes '.
'to a revision) without needing to log in to the web UI.'.
"\n\n".
'Mail commands are keywords which start with an exclamation point, '.
'like `!claim`. Some commands may take parameters, like '.
"`!assign alincoln`.\n\n".
'To use mail commands, write one command per line at the beginning '.
'or end of your mail message. For example, you could write this in a '.
'reply to task email to claim the task:'.
"\n\n```\n!claim\n\nI'll take care of this.\n```\n\n\n".
"When Phabricator receives your mail, it will process any commands ".
"first, then post the remaining message body as a comment. You can ".
"execute multiple commands at once:".
"\n\n```\n!assign alincoln\n!close\n\nI just talked to @alincoln, ".
"and he showed me that he fixed this.\n```\n",
PhabricatorEnv::getDoclink('Configuring Inbound Email'));
$content[] = '= '.$spec['header'];
$content[] = $spec['summary'];
$content[] = '= '.pht('Quick Reference');
$content[] = pht(
'This table summarizes the available mail commands. For details on a '.
'specific command, see the command section below.');
$table = array();
$table[] = '| '.pht('Command').' | '.pht('Summary').' |';
$table[] = '|---|---|';
foreach ($commands as $command) {
$summary = $command->getCommandSummary();
$table[] = '| '.$command->getCommandSyntax().' | '.$summary;
}
$table = implode("\n", $table);
$content[] = $table;
foreach ($commands as $command) {
$content[] = '== !'.$command->getCommand().' ==';
$content[] = $command->getCommandSummary();
$aliases = $command->getCommandAliases();
if ($aliases) {
foreach ($aliases as $key => $alias) {
$aliases[$key] = '!'.$alias;
}
$aliases = implode(', ', $aliases);
} else {
$aliases = '//None//';
}
$syntax = $command->getCommandSyntax();
$table = array();
$table[] = '| '.pht('Property').' | '.pht('Value');
$table[] = '|---|---|';
$table[] = '| **'.pht('Syntax').'** | '.$syntax;
$table[] = '| **'.pht('Aliases').'** | '.$aliases;
$table[] = '| **'.pht('Class').'** | `'.get_class($command).'`';
$table = implode("\n", $table);
$content[] = $table;
$description = $command->getCommandDescription();
if ($description) {
$content[] = $description;
}
}
$content = implode("\n\n", $content);
$title = $spec['name'];
$crumbs = $this->buildApplicationCrumbs();
$this->addApplicationCrumb($crumbs, $selected);
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$content_box = new PHUIRemarkupView($viewer, $content);
$info_view = null;
if (!PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain')) {
$error = pht(
"Phabricator is not currently configured to accept inbound mail. ".
"You won't be able to interact with objects over email until ".
"inbound mail is set up.");
$info_view = id(new PHUIInfoView())
->setErrors(array($error));
}
$header = id(new PHUIHeaderView())
->setHeader($title);
- $document = id(new PHUIDocumentViewPro())
+ $document = id(new PHUIDocumentView())
->setHeader($header)
->appendChild($info_view)
->appendChild($content_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($document);
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
index dfbe89165..8a6b2d227 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
@@ -1,107 +1,128 @@
<?php
abstract class PhabricatorMailImplementationAdapter extends Phobject {
private $key;
private $priority;
private $options = array();
+ private $supportsInbound = true;
+ private $supportsOutbound = true;
+
final public function getAdapterType() {
return $this->getPhobjectClassConstant('ADAPTERTYPE');
}
final public static function getAllAdapters() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAdapterType')
->execute();
}
abstract public function setFrom($email, $name = '');
abstract public function addReplyTo($email, $name = '');
abstract public function addTos(array $emails);
abstract public function addCCs(array $emails);
abstract public function addAttachment($data, $filename, $mimetype);
abstract public function addHeader($header_name, $header_value);
abstract public function setBody($plaintext_body);
abstract public function setHTMLBody($html_body);
abstract public function setSubject($subject);
/**
* Some mailers, notably Amazon SES, do not support us setting a specific
* Message-ID header.
*/
abstract public function supportsMessageIDHeader();
/**
* Send the message. Generally, this means connecting to some service and
* handing data to it.
*
* If the adapter determines that the mail will never be deliverable, it
* should throw a @{class:PhabricatorMetaMTAPermanentFailureException}.
*
* For temporary failures, throw some other exception or return `false`.
*
* @return bool True on success.
*/
abstract public function send();
final public function setKey($key) {
$this->key = $key;
return $this;
}
final public function getKey() {
return $this->key;
}
final public function setPriority($priority) {
$this->priority = $priority;
return $this;
}
final public function getPriority() {
return $this->priority;
}
+ final public function setSupportsInbound($supports_inbound) {
+ $this->supportsInbound = $supports_inbound;
+ return $this;
+ }
+
+ final public function getSupportsInbound() {
+ return $this->supportsInbound;
+ }
+
+ final public function setSupportsOutbound($supports_outbound) {
+ $this->supportsOutbound = $supports_outbound;
+ return $this;
+ }
+
+ final public function getSupportsOutbound() {
+ return $this->supportsOutbound;
+ }
+
final public function getOption($key) {
if (!array_key_exists($key, $this->options)) {
throw new Exception(
pht(
'Mailer ("%s") is attempting to access unknown option ("%s").',
get_class($this),
$key));
}
return $this->options[$key];
}
final public function setOptions(array $options) {
$this->validateOptions($options);
$this->options = $options;
return $this;
}
abstract protected function validateOptions(array $options);
abstract public function newDefaultOptions();
abstract public function newLegacyOptions();
public function prepareForSend() {
return;
}
protected function renderAddress($email, $name = null) {
if (strlen($name)) {
return (string)id(new PhutilEmailAddress())
->setDisplayName($name)
->setAddress($email);
} else {
return $email;
}
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
index 8a8d0de0c..80807398f 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
@@ -1,126 +1,134 @@
<?php
/**
* Mail adapter that doesn't actually send any email, for writing unit tests
* against.
*/
final class PhabricatorMailImplementationTestAdapter
extends PhabricatorMailImplementationAdapter {
const ADAPTERTYPE = 'test';
private $guts = array();
private $config = array();
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array());
}
public function newDefaultOptions() {
return array();
}
public function newLegacyOptions() {
return array();
}
public function prepareForSend(array $config = array()) {
$this->config = $config;
}
public function setFrom($email, $name = '') {
$this->guts['from'] = $email;
$this->guts['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->guts['reply-to'])) {
$this->guts['reply-to'] = array();
}
$this->guts['reply-to'][] = array(
'email' => $email,
'name' => $name,
);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->guts['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->guts['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->guts['attachments'][] = array(
'data' => $data,
'filename' => $filename,
'mimetype' => $mimetype,
);
return $this;
}
public function addHeader($header_name, $header_value) {
$this->guts['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->guts['body'] = $body;
return $this;
}
public function setHTMLBody($html_body) {
$this->guts['html-body'] = $html_body;
return $this;
}
public function setSubject($subject) {
$this->guts['subject'] = $subject;
return $this;
}
public function supportsMessageIDHeader() {
return idx($this->config, 'supportsMessageIDHeader', true);
}
public function send() {
if (!empty($this->guts['fail-permanently'])) {
throw new PhabricatorMetaMTAPermanentFailureException(
pht('Unit Test (Permanent)'));
}
if (!empty($this->guts['fail-temporarily'])) {
throw new Exception(
pht('Unit Test (Temporary)'));
}
$this->guts['did-send'] = true;
return true;
}
public function getGuts() {
return $this->guts;
}
public function setFailPermanently($fail) {
$this->guts['fail-permanently'] = $fail;
return $this;
}
public function setFailTemporarily($fail) {
$this->guts['fail-temporarily'] = $fail;
return $this;
}
+ public function getBody() {
+ return idx($this->guts, 'body');
+ }
+
+ public function getHTMLBody() {
+ return idx($this->guts, 'html-body');
+ }
+
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
index 3ca2711dc..91f656cf9 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
@@ -1,107 +1,110 @@
<?php
final class PhabricatorMetaMTAMailgunReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
private function verifyMessage() {
$request = $this->getRequest();
$timestamp = $request->getStr('timestamp');
$token = $request->getStr('token');
$sig = $request->getStr('signature');
// An install may configure multiple Mailgun mailers, and we might receive
// inbound mail from any of them. Test the signature to see if it matches
// any configured Mailgun mailer.
- $mailers = PhabricatorMetaMTAMail::newMailersWithTypes(
+ $mailers = PhabricatorMetaMTAMail::newMailers(
array(
- PhabricatorMailImplementationMailgunAdapter::ADAPTERTYPE,
+ 'inbound' => true,
+ 'types' => array(
+ PhabricatorMailImplementationMailgunAdapter::ADAPTERTYPE,
+ ),
));
foreach ($mailers as $mailer) {
$api_key = $mailer->getOption('api-key');
$hash = hash_hmac('sha256', $timestamp.$token, $api_key);
if (phutil_hashes_are_identical($sig, $hash)) {
return true;
}
}
return false;
}
public function handleRequest(AphrontRequest $request) {
// No CSRF for Mailgun.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if (!$this->verifyMessage()) {
throw new Exception(
pht('Mail signature is not valid. Check your Mailgun API key.'));
}
$raw_headers = $request->getStr('message-headers');
$raw_dict = array();
if (strlen($raw_headers)) {
$raw_headers = phutil_json_decode($raw_headers);
foreach ($raw_headers as $raw_header) {
list($name, $value) = $raw_header;
$raw_dict[$name] = $value;
}
}
$headers = array(
'to' => $request->getStr('recipient'),
'from' => $request->getStr('from'),
'subject' => $request->getStr('subject'),
) + $raw_dict;
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $request->getStr('stripped-text'),
'html' => $request->getStr('stripped-html'),
));
$file_phids = array();
foreach ($_FILES as $file_raw) {
try {
$file = PhabricatorFile::newFromPHPUpload(
$file_raw,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
try {
$received->save();
$received->processReceivedMail();
} catch (Exception $ex) {
// We can get exceptions here in two cases.
// First, saving the message may throw if we have already received a
// message with the same Message ID. In this case, we're declining to
// process a duplicate message, so failing silently is correct.
// Second, processing the message may throw (for example, if it contains
// an invalid !command). This will generate an email as a side effect,
// so we don't need to explicitly handle the exception here.
// In these cases, we want to return HTTP 200. If we do not, MailGun will
// re-transmit the message later.
phlog($ex);
}
$response = new AphrontWebpageResponse();
$response->setContent(pht("Got it! Thanks, Mailgun!\n"));
return $response;
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
index 345cd93fe..550a1366f 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
@@ -1,102 +1,105 @@
<?php
final class PhabricatorMetaMTAPostmarkReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function handleRequest(AphrontRequest $request) {
// Don't process requests if we don't have a configured Postmark adapter.
- $mailers = PhabricatorMetaMTAMail::newMailersWithTypes(
+ $mailers = PhabricatorMetaMTAMail::newMailers(
array(
- PhabricatorMailImplementationPostmarkAdapter::ADAPTERTYPE,
+ 'inbound' => true,
+ 'types' => array(
+ PhabricatorMailImplementationPostmarkAdapter::ADAPTERTYPE,
+ ),
));
if (!$mailers) {
return new Aphront404Response();
}
$remote_address = $request->getRemoteAddress();
$any_remote_match = false;
foreach ($mailers as $mailer) {
$inbound_addresses = $mailer->getOption('inbound-addresses');
$cidr_list = PhutilCIDRList::newList($inbound_addresses);
if ($cidr_list->containsAddress($remote_address)) {
$any_remote_match = true;
break;
}
}
if (!$any_remote_match) {
return new Aphront400Response();
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$raw_input = PhabricatorStartup::getRawInput();
try {
$data = phutil_json_decode($raw_input);
} catch (Exception $ex) {
return new Aphront400Response();
}
$raw_headers = array();
$header_items = idx($data, 'Headers', array());
foreach ($header_items as $header_item) {
$name = idx($header_item, 'Name');
$value = idx($header_item, 'Value');
$raw_headers[$name] = $value;
}
$headers = array(
'to' => idx($data, 'To'),
'from' => idx($data, 'From'),
'cc' => idx($data, 'Cc'),
'subject' => idx($data, 'Subject'),
) + $raw_headers;
$received = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders($headers)
->setBodies(
array(
'text' => idx($data, 'TextBody'),
'html' => idx($data, 'HtmlBody'),
));
$file_phids = array();
$attachments = idx($data, 'Attachments', array());
foreach ($attachments as $attachment) {
$file_data = idx($attachment, 'Content');
$file_data = base64_decode($file_data);
try {
$file = PhabricatorFile::newFromFileData(
$file_data,
array(
'name' => idx($attachment, 'Name'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
try {
$received->save();
$received->processReceivedMail();
} catch (Exception $ex) {
phlog($ex);
}
return id(new AphrontWebpageResponse())
->setContent(pht("Got it! Thanks, Postmark!\n"));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
index 6651f85d6..9ec32f60a 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
@@ -1,70 +1,73 @@
<?php
final class PhabricatorMetaMTASendGridReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
// SendGrid doesn't sign payloads so we can't be sure that SendGrid
// actually sent this request, but require a configured SendGrid mailer
// before we activate this endpoint.
- $mailers = PhabricatorMetaMTAMail::newMailersWithTypes(
+ $mailers = PhabricatorMetaMTAMail::newMailers(
array(
- PhabricatorMailImplementationSendGridAdapter::ADAPTERTYPE,
+ 'inbound' => true,
+ 'types' => array(
+ PhabricatorMailImplementationSendGridAdapter::ADAPTERTYPE,
+ ),
));
if (!$mailers) {
return new Aphront404Response();
}
// No CSRF for SendGrid.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$user = $request->getUser();
$raw_headers = $request->getStr('headers');
$raw_headers = explode("\n", rtrim($raw_headers));
$raw_dict = array();
foreach (array_filter($raw_headers) as $header) {
list($name, $value) = explode(':', $header, 2);
$raw_dict[$name] = ltrim($value);
}
$headers = array(
'to' => $request->getStr('to'),
'from' => $request->getStr('from'),
'subject' => $request->getStr('subject'),
) + $raw_dict;
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $request->getStr('text'),
'html' => $request->getStr('from'),
));
$file_phids = array();
foreach ($_FILES as $file_raw) {
try {
$file = PhabricatorFile::newFromPHPUpload(
$file_raw,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
$received->save();
$received->processReceivedMail();
$response = new AphrontWebpageResponse();
$response->setContent(pht('Got it! Thanks, SendGrid!')."\n");
return $response;
}
}
diff --git a/src/applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php b/src/applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php
new file mode 100644
index 000000000..4e9e0fe38
--- /dev/null
+++ b/src/applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php
@@ -0,0 +1,28 @@
+<?php
+
+final class PhabricatorMailPropertiesDestructionEngineExtension
+ extends PhabricatorDestructionEngineExtension {
+
+ const EXTENSIONKEY = 'mail.properties';
+
+ public function getExtensionName() {
+ return pht('Mail Properties');
+ }
+
+ public function destroyObject(
+ PhabricatorDestructionEngine $engine,
+ $object) {
+
+ $object_phid = $object->getPHID();
+ $viewer = $engine->getViewer();
+
+ $properties = id(new PhabricatorMetaMTAMailPropertiesQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object_phid))
+ ->executeOne();
+ if ($properties) {
+ $properties->delete();
+ }
+ }
+
+}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
index 3cbe2345e..46c444571 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
@@ -1,164 +1,166 @@
<?php
final class PhabricatorMailManagementReceiveTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('receive-test')
->setSynopsis(
pht(
'Simulate receiving mail. This is primarily useful if you are '.
'developing new mail receivers.'))
->setExamples(
'**receive-test** --as alincoln --to D123 < body.txt')
->setArguments(
array(
array(
'name' => 'as',
'param' => 'user',
'help' => pht('Act as the specified user.'),
),
array(
'name' => 'from',
'param' => 'email',
'help' => pht('Simulate mail delivery "From:" the given user.'),
),
array(
'name' => 'to',
'param' => 'object',
'help' => pht('Simulate mail delivery "To:" the given object.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$to = $args->getArg('to');
if (!$to) {
throw new PhutilArgumentUsageException(
pht(
"Use '%s' to specify the receiving object or email address.",
'--to'));
}
$to_application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($this->getViewer())
->withAddresses(array($to))
->executeOne();
$as = $args->getArg('as');
if (!$as && $to_application_email) {
$default_phid = $to_application_email->getConfigValue(
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
if ($default_phid) {
$default_user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($default_phid))
->executeOne();
if ($default_user) {
$as = $default_user->getUsername();
}
}
}
if (!$as) {
throw new PhutilArgumentUsageException(
pht("Use '--as' to specify the acting user."));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($as))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $as));
}
$from = $args->getArg('from');
if (!$from) {
$from = $user->loadPrimaryEmail()->getAddress();
}
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$received = new PhabricatorMetaMTAReceivedMail();
$header_content = array(
'Message-ID' => Filesystem::readRandomCharacters(12),
'From' => $from,
);
if (preg_match('/.+@.+/', $to)) {
$header_content['to'] = $to;
} else {
// We allow the user to use an object name instead of a real address
// as a convenience. To build the mail, we build a similar message and
// look for a receiver which will accept it.
$pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
$pseudomail = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders(
array(
'to' => $to.'+1+'.$pseudohash,
));
$receivers = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorMailReceiver')
->setFilterMethod('isEnabled')
->execute();
$receiver = null;
foreach ($receivers as $possible_receiver) {
if (!$possible_receiver->canAcceptMail($pseudomail)) {
continue;
}
$receiver = $possible_receiver;
break;
}
if (!$receiver) {
throw new Exception(
pht("No configured mail receiver can accept mail to '%s'.", $to));
}
if (!($receiver instanceof PhabricatorObjectMailReceiver)) {
$class = get_class($receiver);
throw new Exception(
pht(
"Receiver '%s' accepts mail to '%s', but is not a ".
"subclass of PhabricatorObjectMailReceiver.",
$class,
$to));
}
$object = $receiver->loadMailReceiverObject($to, $user);
if (!$object) {
throw new Exception(pht("No such object '%s'!", $to));
}
+ $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
+
$hash = PhabricatorObjectMailReceiver::computeMailHash(
- $object->getMailKey(),
+ $mail_key,
$user->getPHID());
$header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@test.com';
}
$received->setHeaders($header_content);
$received->setBodies(
array(
'text' => $body,
));
$received->save();
$received->processReceivedMail();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-inbound --id %d\n\n",
pht('Mail received! You can view details by running this command:'),
$received->getID());
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
index 152b62fd3..8140d64bd 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
@@ -1,201 +1,209 @@
<?php
final class PhabricatorMailManagementSendTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('send-test')
->setSynopsis(
pht(
'Simulate sending mail. This may be useful to test your mail '.
'configuration, or while developing new mail adapters.'))
->setExamples('**send-test** --to alincoln --subject hi < body.txt')
->setArguments(
array(
array(
'name' => 'from',
'param' => 'user',
'help' => pht('Send mail from the specified user.'),
),
array(
'name' => 'to',
'param' => 'user',
'help' => pht('Send mail "To:" the specified users.'),
'repeat' => true,
),
array(
'name' => 'cc',
'param' => 'user',
'help' => pht('Send mail which "Cc:"s the specified users.'),
'repeat' => true,
),
array(
'name' => 'subject',
'param' => 'text',
'help' => pht('Use the provided subject.'),
),
array(
'name' => 'tag',
'param' => 'text',
'help' => pht('Add the given mail tags.'),
'repeat' => true,
),
array(
'name' => 'attach',
'param' => 'file',
'help' => pht('Attach a file.'),
'repeat' => true,
),
array(
'name' => 'mailer',
'param' => 'key',
'help' => pht('Send with a specific configured mailer.'),
),
array(
'name' => 'html',
'help' => pht('Send as HTML mail.'),
),
array(
'name' => 'bulk',
'help' => pht('Send with bulk headers.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$from = $args->getArg('from');
if ($from) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($from))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $from));
}
$from = $user;
}
$tos = $args->getArg('to');
$ccs = $args->getArg('cc');
if (!$tos && !$ccs) {
throw new PhutilArgumentUsageException(
pht(
'Specify one or more users to send mail to with `%s` and `%s`.',
'--to',
'--cc'));
}
$names = array_merge($tos, $ccs);
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames($names)
->execute();
$users = mpull($users, null, 'getUsername');
$raw_tos = array();
foreach ($tos as $key => $username) {
// If the recipient has an "@" in any noninitial position, treat this as
// a raw email address.
if (preg_match('/.@/', $username)) {
$raw_tos[] = $username;
unset($tos[$key]);
continue;
}
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$tos[$key] = $users[$username]->getPHID();
}
foreach ($ccs as $key => $username) {
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$ccs[$key] = $users[$username]->getPHID();
}
$subject = $args->getArg('subject');
if ($subject === null) {
$subject = pht('No Subject');
}
$tags = $args->getArg('tag');
$attach = $args->getArg('attach');
$is_bulk = $args->getArg('bulk');
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$mail = id(new PhabricatorMetaMTAMail())
->addCCs($ccs)
->setSubject($subject)
->setBody($body)
->setIsBulk($is_bulk)
->setMailTags($tags);
if ($tos) {
$mail->addTos($tos);
}
if ($raw_tos) {
$mail->addRawTos($raw_tos);
}
if ($args->getArg('html')) {
$mail->setBody(
pht(
'(This is a placeholder plaintext email body for a test message '.
'sent with %s.)',
'--html'));
$mail->setHTMLBody($body);
} else {
$mail->setBody($body);
}
if ($from) {
$mail->setFrom($from->getPHID());
}
$mailer_key = $args->getArg('mailer');
if ($mailer_key !== null) {
- $mailers = PhabricatorMetaMTAMail::newMailers();
+ $mailers = PhabricatorMetaMTAMail::newMailers(array());
+
$mailers = mpull($mailers, null, 'getKey');
if (!isset($mailers[$mailer_key])) {
throw new PhutilArgumentUsageException(
pht(
'Mailer key ("%s") is not configured. Available keys are: %s.',
$mailer_key,
implode(', ', array_keys($mailers))));
}
+ if (!$mailers[$mailer_key]->getSupportsOutbound()) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Mailer ("%s") is not configured to support outbound mail.',
+ $mailer_key));
+ }
+
$mail->setTryMailers(array($mailer_key));
}
foreach ($attach as $attachment) {
$data = Filesystem::readFile($attachment);
$name = basename($attachment);
$mime = Filesystem::getMimeType($attachment);
$file = new PhabricatorMetaMTAAttachment($data, $name, $mime);
$mail->addAttachment($file);
}
PhabricatorWorker::setRunAllTasksInProcess(true);
$mail->save();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-outbound --id %d\n\n",
pht('Mail sent! You can view details by running this command:'),
$mail->getID());
}
}
diff --git a/src/applications/almanac/query/AlmanacNetworkQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php
similarity index 53%
copy from src/applications/almanac/query/AlmanacNetworkQuery.php
copy to src/applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php
index a09da0093..e6ec09f4f 100644
--- a/src/applications/almanac/query/AlmanacNetworkQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php
@@ -1,61 +1,51 @@
<?php
-final class AlmanacNetworkQuery
- extends AlmanacQuery {
+final class PhabricatorMetaMTAMailPropertiesQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
- private $phids;
+ private $objectPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
- public function withPHIDs(array $phids) {
- $this->phids = $phids;
+ public function withObjectPHIDs(array $object_phids) {
+ $this->objectPHIDs = $object_phids;
return $this;
}
public function newResultObject() {
- return new AlmanacNetwork();
- }
-
- public function withNameNgrams($ngrams) {
- return $this->withNgramsConstraint(
- new AlmanacNetworkNameNgrams(),
- $ngrams);
+ return new PhabricatorMetaMTAMailProperties();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
- 'network.id IN (%Ld)',
+ 'id IN (%Ld)',
$this->ids);
}
- if ($this->phids !== null) {
+ if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
- 'network.phid IN (%Ls)',
- $this->phids);
+ 'objectPHID IN (%Ls)',
+ $this->objectPHIDs);
}
return $where;
}
- protected function getPrimaryTableAlias() {
- return 'network';
- }
-
public function getQueryApplicationClass() {
- return 'PhabricatorAlmanacApplication';
+ return 'PhabricatorMetaMTAApplication';
}
}
diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
index 5985458ab..801f7a8e6 100644
--- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
@@ -1,208 +1,209 @@
<?php
abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver {
/**
* Return a regular expression fragment which matches the name of an
* object which can receive mail. For example, Differential uses:
*
* D[1-9]\d*
*
* ...to match `D123`, etc., identifying Differential Revisions.
*
* @return string Regular expression fragment.
*/
abstract protected function getObjectPattern();
/**
* Load the object receiving mail, based on an identifying pattern. Normally
* this pattern is some sort of object ID.
*
* @param string A string matched by @{method:getObjectPattern}
* fragment.
* @param PhabricatorUser The viewing user.
* @return void
*/
abstract protected function loadObject($pattern, PhabricatorUser $viewer);
final protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
$object = $this->loadObjectFromMail($mail, $sender);
$mail->setRelatedPHID($object->getPHID());
$this->processReceivedObjectMail($mail, $object, $sender);
return $this;
}
protected function processReceivedObjectMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorLiskDAO $object,
PhabricatorUser $sender) {
$handler = $this->getTransactionReplyHandler();
if ($handler) {
return $handler
->setMailReceiver($object)
->setActor($sender)
->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs())
->processEmail($mail);
}
throw new PhutilMethodNotImplementedException();
}
protected function getTransactionReplyHandler() {
return null;
}
public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) {
return $this->loadObject($pattern, $viewer);
}
public function validateSender(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
parent::validateSender($mail, $sender);
$parts = $this->matchObjectAddressInMail($mail);
$pattern = $parts['pattern'];
try {
$object = $this->loadObjectFromMail($mail, $sender);
} catch (PhabricatorPolicyException $policy_exception) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM,
pht(
'This mail is addressed to an object ("%s") you do not have '.
'permission to see: %s',
$pattern,
$policy_exception->getMessage()));
}
if (!$object) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT,
pht(
'This mail is addressed to an object ("%s"), but that object '.
'does not exist.',
$pattern));
}
$sender_identifier = $parts['sender'];
if ($sender_identifier === 'public') {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL,
pht(
'This mail is addressed to the public email address of an object '.
'("%s"), but public replies are not enabled on this Phabricator '.
'install. An administrator may have recently disabled this '.
'setting, or you may have replied to an old message. Try '.
'replying to a more recent message instead.',
$pattern));
}
$check_phid = $object->getPHID();
} else {
if ($sender_identifier != $sender->getID()) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH,
pht(
'This mail is addressed to the private email address of an object '.
'("%s"), but you are not the user who is authorized to use the '.
'address you sent mail to. Each private address is unique to the '.
'user who received the original mail. Try replying to a message '.
'which was sent directly to you instead.',
$pattern));
}
$check_phid = $sender->getPHID();
}
- $expect_hash = self::computeMailHash($object->getMailKey(), $check_phid);
+ $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
+ $expect_hash = self::computeMailHash($mail_key, $check_phid);
if (!phutil_hashes_are_identical($expect_hash, $parts['hash'])) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH,
pht(
'This mail is addressed to an object ("%s"), but the address is '.
'not correct (the security hash is wrong). Check that the address '.
'is correct.',
$pattern));
}
}
final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
if ($this->matchObjectAddressInMail($mail)) {
return true;
}
return false;
}
private function matchObjectAddressInMail(
PhabricatorMetaMTAReceivedMail $mail) {
foreach ($mail->getToAddresses() as $address) {
$parts = $this->matchObjectAddress($address);
if ($parts) {
return $parts;
}
}
return null;
}
private function matchObjectAddress($address) {
$regexp = $this->getAddressRegexp();
$address = self::stripMailboxPrefix($address);
$local = id(new PhutilEmailAddress($address))->getLocalPart();
$matches = null;
if (!preg_match($regexp, $local, $matches)) {
return false;
}
return $matches;
}
private function getAddressRegexp() {
$pattern = $this->getObjectPattern();
$regexp =
'(^'.
'(?P<pattern>'.$pattern.')'.
'\\+'.
'(?P<sender>\w+)'.
'\\+'.
'(?P<hash>[a-f0-9]{16})'.
'$)Ui';
return $regexp;
}
private function loadObjectFromMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
$parts = $this->matchObjectAddressInMail($mail);
return $this->loadObject(
phutil_utf8_strtoupper($parts['pattern']),
$sender);
}
public static function computeMailHash($mail_key, $phid) {
$global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
$hash = PhabricatorHash::weakDigest($mail_key.$global_mail_key.$phid);
return substr($hash, 0, 16);
}
}
diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
index 2c2e0561c..a697436d2 100644
--- a/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
+++ b/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
@@ -1,126 +1,129 @@
<?php
final class PhabricatorObjectMailReceiverTestCase
extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testDropUnconfiguredPublicMail() {
list($task, $user, $mail) = $this->buildMail('public');
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('metamta.public-replies', false);
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL,
$mail->getStatus());
}
/*
TODO: Tasks don't support policies yet. Implement this once they do.
public function testDropPolicyViolationMail() {
list($task, $user, $mail) = $this->buildMail('public');
// TODO: Set task policy to "no one" here.
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM,
$mail->getStatus());
}
*/
public function testDropInvalidObjectMail() {
list($task, $user, $mail) = $this->buildMail('404');
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT,
$mail->getStatus());
}
public function testDropUserMismatchMail() {
list($task, $user, $mail) = $this->buildMail('baduser');
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH,
$mail->getStatus());
}
public function testDropHashMismatchMail() {
list($task, $user, $mail) = $this->buildMail('badhash');
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH,
$mail->getStatus());
}
private function buildMail($style) {
$user = $this->generateNewTestUser();
$task = id(new PhabricatorManiphestTaskTestDataGenerator())
->setViewer($user)
->generateObject();
$is_public = ($style === 'public');
$is_bad_hash = ($style == 'badhash');
$is_bad_user = ($style == 'baduser');
$is_404_object = ($style == '404');
if ($is_public) {
$user_identifier = 'public';
} else if ($is_bad_user) {
$user_identifier = $user->getID() + 1;
} else {
$user_identifier = $user->getID();
}
if ($is_bad_hash) {
$hash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
} else {
+
+ $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($task);
+
$hash = PhabricatorObjectMailReceiver::computeMailHash(
- $task->getMailKey(),
+ $mail_key,
$is_public ? $task->getPHID() : $user->getPHID());
}
if ($is_404_object) {
$task_identifier = 'T'.($task->getID() + 1);
} else {
$task_identifier = 'T'.$task->getID();
}
$to = $task_identifier.'+'.$user_identifier.'+'.$hash.'@example.com';
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'From' => $user->loadPrimaryEmail()->getAddress(),
'To' => $to,
));
return array($task, $user, $mail);
}
}
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index f8dd784e3..70b524b52 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,436 +1,442 @@
<?php
abstract class PhabricatorMailReplyHandler extends Phobject {
private $mailReceiver;
private $applicationEmail;
private $actor;
private $excludePHIDs = array();
private $unexpandablePHIDs = array();
final public function setMailReceiver($mail_receiver) {
$this->validateMailReceiver($mail_receiver);
$this->mailReceiver = $mail_receiver;
return $this;
}
final public function getMailReceiver() {
return $this->mailReceiver;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final public function getActor() {
return $this->actor;
}
final public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
final public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
public function setUnexpandablePHIDs(array $phids) {
$this->unexpandablePHIDs = $phids;
return $this;
}
public function getUnexpandablePHIDs() {
return $this->unexpandablePHIDs;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorUser $user);
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
}
abstract protected function receiveEmail(
PhabricatorMetaMTAReceivedMail $mail);
public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
$this->dropEmptyMail($mail);
return $this->receiveEmail($mail);
}
private function dropEmptyMail(PhabricatorMetaMTAReceivedMail $mail) {
$body = $mail->getCleanTextBody();
$attachments = $mail->getAttachments();
if (strlen($body) || $attachments) {
return;
}
// Only send an error email if the user is talking to just Phabricator.
// We can assume if there is only one "To" address it is a Phabricator
// address since this code is running and everything.
$is_direct_mail = (count($mail->getToAddresses()) == 1) &&
(count($mail->getCCAddresses()) == 0);
if ($is_direct_mail) {
$status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY;
} else {
$status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED;
}
throw new PhabricatorMetaMTAReceivedMailProcessingException(
$status_code,
pht(
'Your message does not contain any body text or attachments, so '.
'Phabricator can not do anything useful with it. Make sure comment '.
'text appears at the top of your message: quoted replies, inline '.
'text, and signatures are discarded and ignored.'));
}
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain() &&
!$this->supportsPublicReplies();
}
public function supportsPublicReplies() {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
return false;
}
if (!$this->getReplyHandlerDomain()) {
return false;
}
return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
$this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$domain = $this->getReplyHandlerDomain();
// We compute a hash using the object's own PHID to prevent an attacker
// from blindly interacting with objects that they haven't ever received
// mail about by just sending to D1@, D2@, etc...
+
+ $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);
+
$hash = PhabricatorObjectMailReceiver::computeMailHash(
- $receiver->getMailKey(),
+ $mail_key,
$receiver->getPHID());
$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
protected function getSingleReplyHandlerPrefix($address) {
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
return ($single_handle_prefix)
? $single_handle_prefix.'+'.$address
: $address;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorUser $user,
$prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $user->getID();
+
+ $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);
+
$hash = PhabricatorObjectMailReceiver::computeMailHash(
- $receiver->getMailKey(),
+ $mail_key,
$user->getPHID());
$domain = $this->getReplyHandlerDomain();
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
final protected function enhanceBodyWithAttachments(
$body,
array $attachments) {
if (!$attachments) {
return $body;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($attachments)
->execute();
$output = array();
$output[] = $body;
// We're going to put all the non-images first in a list, then embed
// the images.
$head = array();
$tail = array();
foreach ($files as $file) {
if ($file->isViewableImage()) {
$tail[] = $file;
} else {
$head[] = $file;
}
}
if ($head) {
$list = array();
foreach ($head as $file) {
$list[] = ' - {'.$file->getMonogram().', layout=link}';
}
$output[] = implode("\n", $list);
}
if ($tail) {
$list = array();
foreach ($tail as $file) {
$list[] = '{'.$file->getMonogram().'}';
}
$output[] = implode("\n\n", $list);
}
$output = implode("\n\n", $output);
return rtrim($output);
}
/**
* Produce a list of mail targets for a given to/cc list.
*
* Each target should be sent a separate email, and contains the information
* required to generate it with appropriate permissions and configuration.
*
* @param list<phid> List of "To" PHIDs.
* @param list<phid> List of "CC" PHIDs.
* @return list<PhabricatorMailTarget> List of targets.
*/
final public function getMailTargets(array $raw_to, array $raw_cc) {
list($to, $cc) = $this->expandRecipientPHIDs($raw_to, $raw_cc);
list($to, $cc) = $this->loadRecipientUsers($to, $cc);
list($to, $cc) = $this->filterRecipientUsers($to, $cc);
if (!$to && !$cc) {
return array();
}
$template = id(new PhabricatorMailTarget())
->setRawToPHIDs($raw_to)
->setRawCCPHIDs($raw_cc);
// Set the public reply address as the default, if one exists. We
// might replace this with a private address later.
if ($this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
if ($reply_to) {
$template->setReplyTo($reply_to);
}
}
$supports_private_replies = $this->supportsPrivateReplies();
$mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
$targets = array();
if ($mail_all) {
$target = id(clone $template)
->setViewer(PhabricatorUser::getOmnipotentUser())
->setToMap($to)
->setCCMap($cc);
$targets[] = $target;
} else {
$map = $to + $cc;
foreach ($map as $phid => $user) {
// Preserve the original To/Cc information on the target.
if (isset($to[$phid])) {
$target_to = array($phid => $user);
$target_cc = array();
} else {
$target_to = array();
$target_cc = array($phid => $user);
}
$target = id(clone $template)
->setViewer($user)
->setToMap($target_to)
->setCCMap($target_cc);
if ($supports_private_replies) {
$reply_to = $this->getPrivateReplyHandlerEmailAddress($user);
if ($reply_to) {
$target->setReplyTo($reply_to);
}
}
$targets[] = $target;
}
}
return $targets;
}
/**
* Expand lists of recipient PHIDs.
*
* This takes any compound recipients (like projects) and looks up all their
* members.
*
* @param list<phid> List of To PHIDs.
* @param list<phid> List of CC PHIDs.
* @return pair<list<phid>, list<phid>> Expanded PHID lists.
*/
private function expandRecipientPHIDs(array $to, array $cc) {
$to_result = array();
$cc_result = array();
// "Unexpandable" users have disengaged from an object (for example,
// by resigning from a revision).
// If such a user is still a direct recipient (for example, they're still
// on the Subscribers list) they're fair game, but group targets (like
// projects) will no longer include them when expanded.
$unexpandable = $this->getUnexpandablePHIDs();
$unexpandable = array_fuse($unexpandable);
$all_phids = array_merge($to, $cc);
if ($all_phids) {
$map = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
foreach ($to as $phid) {
foreach ($map[$phid] as $expanded) {
if ($expanded !== $phid) {
if (isset($unexpandable[$expanded])) {
continue;
}
}
$to_result[$expanded] = $expanded;
}
}
foreach ($cc as $phid) {
foreach ($map[$phid] as $expanded) {
if ($expanded !== $phid) {
if (isset($unexpandable[$expanded])) {
continue;
}
}
$cc_result[$expanded] = $expanded;
}
}
}
// Remove recipients from "CC" if they're also present in "To".
$cc_result = array_diff_key($cc_result, $to_result);
return array(array_values($to_result), array_values($cc_result));
}
/**
* Load @{class:PhabricatorUser} objects for each recipient.
*
* Invalid recipients are dropped from the results.
*
* @param list<phid> List of To PHIDs.
* @param list<phid> List of CC PHIDs.
* @return pair<wild, wild> Maps from PHIDs to users.
*/
private function loadRecipientUsers(array $to, array $cc) {
$to_result = array();
$cc_result = array();
$all_phids = array_merge($to, $cc);
if ($all_phids) {
// We need user settings here because we'll check translations later
// when generating mail.
$users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->needUserSettings(true)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($to as $phid) {
if (isset($users[$phid])) {
$to_result[$phid] = $users[$phid];
}
}
foreach ($cc as $phid) {
if (isset($users[$phid])) {
$cc_result[$phid] = $users[$phid];
}
}
}
return array($to_result, $cc_result);
}
/**
* Remove recipients who do not have permission to view the mail receiver.
*
* @param map<string, PhabricatorUser> Map of "To" users.
* @param map<string, PhabricatorUser> Map of "CC" users.
* @return pair<wild, wild> Filtered user maps.
*/
private function filterRecipientUsers(array $to, array $cc) {
$to_result = array();
$cc_result = array();
$all_users = $to + $cc;
if ($all_users) {
$can_see = array();
$object = $this->getMailReceiver();
foreach ($all_users as $phid => $user) {
$visible = PhabricatorPolicyFilter::hasCapability(
$user,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($visible) {
$can_see[$phid] = true;
}
}
foreach ($to as $phid => $user) {
if (!empty($can_see[$phid])) {
$to_result[$phid] = $all_users[$phid];
}
}
foreach ($cc as $phid => $user) {
if (!empty($can_see[$phid])) {
$cc_result[$phid] = $all_users[$phid];
}
}
}
return array($to_result, $cc_result);
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index 5326e1063..7d758ee91 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,1549 +1,1642 @@
<?php
/**
* @task recipients Managing Recipients
*/
final class PhabricatorMetaMTAMail
extends PhabricatorMetaMTADAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const RETRY_DELAY = 5;
protected $actorPHID;
protected $parameters = array();
protected $status;
protected $message;
protected $relatedPHID;
private $recipientExpansionMap;
private $routingMap;
public function __construct() {
$this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE;
$this->parameters = array(
'sensitive' => true,
'mustEncrypt' => false,
);
parent::__construct();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'actorPHID' => 'phid?',
'status' => 'text32',
'relatedPHID' => 'phid?',
// T6203/NULLABILITY
// This should just be empty if there's no body.
'message' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'status' => array(
'columns' => array('status'),
),
'key_actorPHID' => array(
'columns' => array('actorPHID'),
),
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorMetaMTAMailPHIDType::TYPECONST);
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param, $default = null) {
// Some old mail was saved without parameters because no parameters were
// set or encoding failed. Recover in these cases so we can perform
// mail migrations, see T9251.
if (!is_array($this->parameters)) {
$this->parameters = array();
}
return idx($this->parameters, $param, $default);
}
/**
* These tags are used to allow users to opt out of receiving certain types
* of mail, like updates when a task's projects change.
*
* @param list<const>
* @return this
*/
public function setMailTags(array $tags) {
$this->setParam('mailtags', array_unique($tags));
return $this;
}
public function getMailTags() {
return $this->getParam('mailtags', array());
}
/**
* In Gmail, conversations will be broken if you reply to a thread and the
* server sends back a response without referencing your Message-ID, even if
* it references a Message-ID earlier in the thread. To avoid this, use the
* parent email's message ID explicitly if it's available. This overwrites the
* "In-Reply-To" and "References" headers we would otherwise generate. This
* needs to be set whenever an action is triggered by an email message. See
* T251 for more details.
*
* @param string The "Message-ID" of the email which precedes this one.
* @return this
*/
public function setParentMessageID($id) {
$this->setParam('parent-message-id', $id);
return $this;
}
public function getParentMessageID() {
return $this->getParam('parent-message-id');
}
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$phids = array_unique($phids);
$this->setParam('to', $phids);
return $this;
}
public function addRawTos(array $raw_email) {
// Strip addresses down to bare emails, since the MailAdapter API currently
// requires we pass it just the address (like `alincoln@logcabin.org`), not
// a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
foreach ($raw_email as $key => $email) {
$object = new PhutilEmailAddress($email);
$raw_email[$key] = $object->getAddress();
}
$this->setParam('raw-to', $raw_email);
return $this;
}
public function addCCs(array $phids) {
$phids = array_unique($phids);
$this->setParam('cc', $phids);
return $this;
}
public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->setParam('exclude', $exclude);
return $this;
}
private function getExcludeMailRecipientPHIDs() {
return $this->getParam('exclude', array());
}
public function setMutedPHIDs(array $muted) {
$this->setParam('muted', $muted);
return $this;
}
private function getMutedPHIDs() {
return $this->getParam('muted', array());
}
public function setForceHeraldMailRecipientPHIDs(array $force) {
$this->setParam('herald-force-recipients', $force);
return $this;
}
private function getForceHeraldMailRecipientPHIDs() {
return $this->getParam('herald-force-recipients', array());
}
public function addPHIDHeaders($name, array $phids) {
$phids = array_unique($phids);
foreach ($phids as $phid) {
$this->addHeader($name, '<'.$phid.'>');
}
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][] = array($name, $value);
return $this;
}
public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
$this->parameters['attachments'][] = $attachment->toDictionary();
return $this;
}
public function getAttachments() {
$dicts = $this->getParam('attachments');
$result = array();
foreach ($dicts as $dict) {
$result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict);
}
return $result;
}
public function getAttachmentFilePHIDs() {
$file_phids = array();
$dictionaries = $this->getParam('attachments');
if ($dictionaries) {
foreach ($dictionaries as $dictionary) {
$file_phid = idx($dictionary, 'filePHID');
if ($file_phid) {
$file_phids[] = $file_phid;
}
}
}
return $file_phids;
}
public function loadAttachedFiles(PhabricatorUser $viewer) {
$file_phids = $this->getAttachmentFilePHIDs();
if (!$file_phids) {
return array();
}
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
}
public function setAttachments(array $attachments) {
assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
$this->setParam('attachments', mpull($attachments, 'toDictionary'));
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
$this->setActorPHID($from);
return $this;
}
public function getFrom() {
return $this->getParam('from');
}
public function setRawFrom($raw_email, $raw_name) {
$this->setParam('raw-from', array($raw_email, $raw_name));
return $this;
}
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setSubjectPrefix($prefix) {
$this->setParam('subject-prefix', $prefix);
return $this;
}
public function setVarySubjectPrefix($prefix) {
$this->setParam('vary-subject-prefix', $prefix);
return $this;
}
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function setSensitiveContent($bool) {
$this->setParam('sensitive', $bool);
return $this;
}
public function hasSensitiveContent() {
return $this->getParam('sensitive', true);
}
public function setMustEncrypt($bool) {
- $this->setParam('mustEncrypt', $bool);
- return $this;
+ return $this->setParam('mustEncrypt', $bool);
}
public function getMustEncrypt() {
return $this->getParam('mustEncrypt', false);
}
+ public function setMustEncryptURI($uri) {
+ return $this->setParam('mustEncrypt.uri', $uri);
+ }
+
+ public function getMustEncryptURI() {
+ return $this->getParam('mustEncrypt.uri');
+ }
+
+ public function setMustEncryptSubject($subject) {
+ return $this->setParam('mustEncrypt.subject', $subject);
+ }
+
+ public function getMustEncryptSubject() {
+ return $this->getParam('mustEncrypt.subject');
+ }
+
public function setMustEncryptReasons(array $reasons) {
- $this->setParam('mustEncryptReasons', $reasons);
- return $this;
+ return $this->setParam('mustEncryptReasons', $reasons);
}
public function getMustEncryptReasons() {
return $this->getParam('mustEncryptReasons', array());
}
public function setMailStamps(array $stamps) {
return $this->setParam('stamps', $stamps);
}
public function getMailStamps() {
return $this->getParam('stamps', array());
}
public function setMailStampMetadata($metadata) {
return $this->setParam('stampMetadata', $metadata);
}
public function getMailStampMetadata() {
return $this->getParam('stampMetadata', array());
}
public function getMailerKey() {
return $this->getParam('mailer.key');
}
public function setTryMailers(array $mailers) {
return $this->setParam('mailers.try', $mailers);
}
public function setHTMLBody($html) {
$this->setParam('html-body', $html);
return $this;
}
public function getBody() {
return $this->getParam('body');
}
public function getHTMLBody() {
return $this->getParam('html-body');
}
public function setIsErrorEmail($is_error) {
$this->setParam('is-error', $is_error);
return $this;
}
public function getIsErrorEmail() {
return $this->getParam('is-error', false);
}
public function getToPHIDs() {
return $this->getParam('to', array());
}
public function getRawToAddresses() {
return $this->getParam('raw-to', array());
}
public function getCcPHIDs() {
return $this->getParam('cc', array());
}
/**
* Force delivery of a message, even if recipients have preferences which
* would otherwise drop the message.
*
* This is primarily intended to let users who don't want any email still
* receive things like password resets.
*
* @param bool True to force delivery despite user preferences.
* @return this
*/
public function setForceDelivery($force) {
$this->setParam('force', $force);
return $this;
}
public function getForceDelivery() {
return $this->getParam('force', false);
}
/**
* Flag that this is an auto-generated bulk message and should have bulk
* headers added to it if appropriate. Broadly, this means some flavor of
* "Precedence: bulk" or similar, but is implementation and configuration
* dependent.
*
* @param bool True if the mail is automated bulk mail.
* @return this
*/
public function setIsBulk($is_bulk) {
$this->setParam('is-bulk', $is_bulk);
return $this;
}
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string Unique identifier, appropriate for use in a Message-ID,
* In-Reply-To or References headers.
* @param bool If true, indicates this is the first message in the thread.
* @return this
*/
public function setThreadID($thread_id, $is_first_message = false) {
$this->setParam('thread-id', $thread_id);
$this->setParam('is-first-message', $is_first_message);
return $this;
}
/**
* Save a newly created mail to the database. The mail will eventually be
* delivered by the MetaMTA daemon.
*
* @return this
*/
public function saveAndSend() {
return $this->save();
}
/**
* @return this
*/
public function save() {
if ($this->getID()) {
return parent::save();
}
// NOTE: When mail is sent from CLI scripts that run tasks in-process, we
// may re-enter this method from within scheduleTask(). The implementation
// is intended to avoid anything awkward if we end up reentering this
// method.
$this->openTransaction();
// Save to generate a mail ID and PHID.
$result = parent::save();
// Write the recipient edges.
$editor = new PhabricatorEdgeEditor();
$edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
$recipient_phids = array_merge(
$this->getToPHIDs(),
$this->getCcPHIDs());
$expanded_phids = $this->expandRecipients($recipient_phids);
$all_phids = array_unique(array_merge(
$recipient_phids,
$expanded_phids));
foreach ($all_phids as $curr_phid) {
$editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
}
$editor->save();
$this->saveTransaction();
// Queue a task to send this mail.
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$this->getID(),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
return $result;
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @return void
*/
public function sendNow() {
if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
throw new Exception(pht('Trying to send an already-sent mail!'));
}
- $mailers = self::newMailers();
+ $mailers = self::newMailers(
+ array(
+ 'outbound' => true,
+ ));
$try_mailers = $this->getParam('mailers.try');
if ($try_mailers) {
$mailers = mpull($mailers, null, 'getKey');
$mailers = array_select_keys($mailers, $try_mailers);
}
return $this->sendWithMailers($mailers);
}
- public static function newMailersWithTypes(array $types) {
- $mailers = self::newMailers();
- $types = array_fuse($types);
-
- foreach ($mailers as $key => $mailer) {
- $mailer_type = $mailer->getAdapterType();
- if (!isset($types[$mailer_type])) {
- unset($mailers[$key]);
- }
- }
-
- return array_values($mailers);
- }
+ public static function newMailers(array $constraints) {
+ PhutilTypeSpec::checkMap(
+ $constraints,
+ array(
+ 'types' => 'optional list<string>',
+ 'inbound' => 'optional bool',
+ 'outbound' => 'optional bool',
+ ));
- public static function newMailers() {
$mailers = array();
$config = PhabricatorEnv::getEnvConfig('cluster.mailers');
if ($config === null) {
$mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
$defaults = $mailer->newDefaultOptions();
$options = $mailer->newLegacyOptions();
$options = $options + $defaults;
$mailer
->setKey('default')
->setPriority(-1)
->setOptions($options);
$mailers[] = $mailer;
} else {
$adapters = PhabricatorMailImplementationAdapter::getAllAdapters();
$next_priority = -1;
foreach ($config as $spec) {
$type = $spec['type'];
if (!isset($adapters[$type])) {
throw new Exception(
pht(
'Unknown mailer ("%s")!',
$type));
}
$key = $spec['key'];
$mailer = id(clone $adapters[$type])
->setKey($key);
$priority = idx($spec, 'priority');
if (!$priority) {
$priority = $next_priority;
$next_priority--;
}
$mailer->setPriority($priority);
$defaults = $mailer->newDefaultOptions();
$options = idx($spec, 'options', array()) + $defaults;
$mailer->setOptions($options);
+ $mailer->setSupportsInbound(idx($spec, 'inbound', true));
+ $mailer->setSupportsOutbound(idx($spec, 'outbound', true));
+
$mailers[] = $mailer;
}
}
+ // Remove mailers with the wrong types.
+ if (isset($constraints['types'])) {
+ $types = $constraints['types'];
+ $types = array_fuse($types);
+ foreach ($mailers as $key => $mailer) {
+ $mailer_type = $mailer->getAdapterType();
+ if (!isset($types[$mailer_type])) {
+ unset($mailers[$key]);
+ }
+ }
+ }
+
+ // If we're only looking for inbound mailers, remove mailers with inbound
+ // support disabled.
+ if (!empty($constraints['inbound'])) {
+ foreach ($mailers as $key => $mailer) {
+ if (!$mailer->getSupportsInbound()) {
+ unset($mailers[$key]);
+ }
+ }
+ }
+
+ // If we're only looking for outbound mailers, remove mailers with outbound
+ // support disabled.
+ if (!empty($constraints['outbound'])) {
+ foreach ($mailers as $key => $mailer) {
+ if (!$mailer->getSupportsOutbound()) {
+ unset($mailers[$key]);
+ }
+ }
+ }
+
$sorted = array();
$groups = mgroup($mailers, 'getPriority');
krsort($groups);
foreach ($groups as $group) {
// Reorder services within the same priority group randomly.
shuffle($group);
foreach ($group as $mailer) {
$sorted[] = $mailer;
}
}
foreach ($sorted as $mailer) {
$mailer->prepareForSend();
}
return $sorted;
}
public function sendWithMailers(array $mailers) {
if (!$mailers) {
+ $any_mailers = self::newMailers(array());
+
+ // NOTE: We can end up here with some custom list of "$mailers", like
+ // from a unit test. In that case, this message could be misleading. We
+ // can't really tell if the caller made up the list, so just assume they
+ // aren't tricking us.
+
+ if ($any_mailers) {
+ $void_message = pht(
+ 'No configured mailers support sending outbound mail.');
+ } else {
+ $void_message = pht(
+ 'No mailers are configured.');
+ }
+ }
+
return $this
->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
- ->setMessage(pht('No mailers are configured.'))
+ ->setMessage($void_message)
->save();
}
$exceptions = array();
foreach ($mailers as $template_mailer) {
$mailer = null;
try {
$mailer = $this->buildMailer($template_mailer);
} catch (Exception $ex) {
$exceptions[] = $ex;
continue;
}
if (!$mailer) {
// If we don't get a mailer back, that means the mail doesn't
// actually need to be sent (for example, because recipients have
// declined to receive the mail). Void it and return.
return $this
->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
->save();
}
try {
$ok = $mailer->send();
if (!$ok) {
// TODO: At some point, we should clean this up and make all mailers
// throw.
throw new Exception(
pht(
'Mail adapter encountered an unexpected, unspecified '.
'failure.'));
}
} catch (PhabricatorMetaMTAPermanentFailureException $ex) {
// If any mailer raises a permanent failure, stop trying to send the
// mail with other mailers.
$this
->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
->setMessage($ex->getMessage())
->save();
throw $ex;
} catch (Exception $ex) {
$exceptions[] = $ex;
continue;
}
// Keep track of which mailer actually ended up accepting the message.
$mailer_key = $mailer->getKey();
if ($mailer_key !== null) {
$this->setParam('mailer.key', $mailer_key);
}
return $this
->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT)
->save();
}
// If we make it here, no mailer could send the mail but no mailer failed
// permanently either. We update the error message for the mail, but leave
// it in the current status (usually, STATUS_QUEUE) and try again later.
$messages = array();
foreach ($exceptions as $ex) {
$messages[] = $ex->getMessage();
}
$messages = implode("\n\n", $messages);
$this
->setMessage($messages)
->save();
if (count($exceptions) === 1) {
throw head($exceptions);
}
throw new PhutilAggregateException(
pht('Encountered multiple exceptions while transmitting mail.'),
$exceptions);
}
private function buildMailer(PhabricatorMailImplementationAdapter $mailer) {
$headers = $this->generateHeaders();
$params = $this->parameters;
$actors = $this->loadAllActors();
$deliverable_actors = $this->filterDeliverableActors($actors);
$default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
if (empty($params['from'])) {
$mailer->setFrom($default_from);
}
$is_first = idx($params, 'is-first-message');
unset($params['is-first-message']);
$is_threaded = (bool)idx($params, 'thread-id');
$must_encrypt = $this->getMustEncrypt();
$reply_to_name = idx($params, 'reply-to-name', '');
unset($params['reply-to-name']);
$add_cc = array();
$add_to = array();
// If we're sending one mail to everyone, some recipients will be in
// "Cc" rather than "To". We'll move them to "To" later (or supply a
// dummy "To") but need to look for the recipient in either the
// "To" or "Cc" fields here.
$target_phid = head(idx($params, 'to', array()));
if (!$target_phid) {
$target_phid = head(idx($params, 'cc', array()));
}
$preferences = $this->loadPreferences($target_phid);
foreach ($params as $key => $value) {
switch ($key) {
case 'raw-from':
list($from_email, $from_name) = $value;
$mailer->setFrom($from_email, $from_name);
break;
case 'from':
// If the mail content must be encrypted, disguise the sender.
if ($must_encrypt) {
$mailer->setFrom($default_from, pht('Phabricator'));
break;
}
$from = $value;
$actor_email = null;
$actor_name = null;
$actor = idx($actors, $from);
if ($actor) {
$actor_email = $actor->getEmailAddress();
$actor_name = $actor->getName();
}
$can_send_as_user = $actor_email &&
PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
if ($can_send_as_user) {
$mailer->setFrom($actor_email, $actor_name);
} else {
$from_email = coalesce($actor_email, $default_from);
$from_name = coalesce($actor_name, pht('Phabricator'));
if (empty($params['reply-to'])) {
$params['reply-to'] = $from_email;
$params['reply-to-name'] = $from_name;
}
$mailer->setFrom($default_from, $from_name);
}
break;
case 'reply-to':
$mailer->addReplyTo($value, $reply_to_name);
break;
case 'to':
$to_phids = $this->expandRecipients($value);
$to_actors = array_select_keys($deliverable_actors, $to_phids);
$add_to = array_merge(
$add_to,
mpull($to_actors, 'getEmailAddress'));
break;
case 'raw-to':
$add_to = array_merge($add_to, $value);
break;
case 'cc':
$cc_phids = $this->expandRecipients($value);
$cc_actors = array_select_keys($deliverable_actors, $cc_phids);
$add_cc = array_merge(
$add_cc,
mpull($cc_actors, 'getEmailAddress'));
break;
case 'attachments':
$attached_viewer = PhabricatorUser::getOmnipotentUser();
$files = $this->loadAttachedFiles($attached_viewer);
foreach ($files as $file) {
$file->attachToObject($this->getPHID());
}
// If the mail content must be encrypted, don't add attachments.
if ($must_encrypt) {
break;
}
$value = $this->getAttachments();
foreach ($value as $attachment) {
$mailer->addAttachment(
$attachment->getData(),
$attachment->getFilename(),
$attachment->getMimeType());
}
break;
case 'subject':
$subject = array();
if ($is_threaded) {
if ($this->shouldAddRePrefix($preferences)) {
$subject[] = 'Re:';
}
}
$subject[] = trim(idx($params, 'subject-prefix'));
// If mail content must be encrypted, we replace the subject with
// a generic one.
if ($must_encrypt) {
- $subject[] = pht('Object Updated');
+ $encrypt_subject = $this->getMustEncryptSubject();
+ if (!strlen($encrypt_subject)) {
+ $encrypt_subject = pht('Object Updated');
+ }
+ $subject[] = $encrypt_subject;
} else {
$vary_prefix = idx($params, 'vary-subject-prefix');
if ($vary_prefix != '') {
if ($this->shouldVarySubject($preferences)) {
$subject[] = $vary_prefix;
}
}
$subject[] = $value;
}
$mailer->setSubject(implode(' ', array_filter($subject)));
break;
case 'thread-id':
// NOTE: Gmail freaks out about In-Reply-To and References which
// aren't in the form "<string@domain.tld>"; this is also required
// by RFC 2822, although some clients are more liberal in what they
// accept.
$domain = PhabricatorEnv::getEnvConfig('metamta.domain');
$value = '<'.$value.'@'.$domain.'>';
if ($is_first && $mailer->supportsMessageIDHeader()) {
$headers[] = array('Message-ID', $value);
} else {
$in_reply_to = $value;
$references = array($value);
$parent_id = $this->getParentMessageID();
if ($parent_id) {
$in_reply_to = $parent_id;
// By RFC 2822, the most immediate parent should appear last
// in the "References" header, so this order is intentional.
$references[] = $parent_id;
}
$references = implode(' ', $references);
$headers[] = array('In-Reply-To', $in_reply_to);
$headers[] = array('References', $references);
}
$thread_index = $this->generateThreadIndex($value, $is_first);
$headers[] = array('Thread-Index', $thread_index);
break;
default:
// Other parameters are handled elsewhere or are not relevant to
// constructing the message.
break;
}
}
$stamps = $this->getMailStamps();
if ($stamps) {
$headers[] = array('X-Phabricator-Stamps', implode(' ', $stamps));
}
$raw_body = idx($params, 'body', '');
$body = $raw_body;
if ($must_encrypt) {
$parts = array();
+
+ $encrypt_uri = $this->getMustEncryptURI();
+ if (!strlen($encrypt_uri)) {
+ $encrypt_phid = $this->getRelatedPHID();
+ if ($encrypt_phid) {
+ $encrypt_uri = urisprintf(
+ '/object/%s/',
+ $encrypt_phid);
+ }
+ }
+
+ if (strlen($encrypt_uri)) {
+ $parts[] = pht(
+ 'This secure message is notifying you of a change to this object:');
+ $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri);
+ }
+
$parts[] = pht(
'The content for this message can only be transmitted over a '.
'secure channel. To view the message content, follow this '.
'link:');
$parts[] = PhabricatorEnv::getProductionURI($this->getURI());
$body = implode("\n\n", $parts);
} else {
$body = $raw_body;
}
- $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
- if (strlen($body) > $max) {
+ $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
+ if (strlen($body) > $body_limit) {
$body = id(new PhutilUTF8StringTruncator())
- ->setMaximumBytes($max)
+ ->setMaximumBytes($body_limit)
->truncateString($body);
$body .= "\n";
- $body .= pht('(This email was truncated at %d bytes.)', $max);
+ $body .= pht('(This email was truncated at %d bytes.)', $body_limit);
}
$mailer->setBody($body);
+ $body_limit -= strlen($body);
// If we sent a different message body than we were asked to, record
// what we actually sent to make debugging and diagnostics easier.
if ($body !== $raw_body) {
$this->setParam('body.sent', $body);
}
if ($must_encrypt) {
$send_html = false;
} else {
$send_html = $this->shouldSendHTML($preferences);
}
- if ($send_html && isset($params['html-body'])) {
- $mailer->setHTMLBody($params['html-body']);
+ if ($send_html) {
+ $html_body = idx($params, 'html-body');
+ if (strlen($html_body)) {
+ // NOTE: We just drop the entire HTML body if it won't fit. Safely
+ // truncating HTML is hard, and we already have the text body to fall
+ // back to.
+ if (strlen($html_body) <= $body_limit) {
+ $mailer->setHTMLBody($html_body);
+ $body_limit -= strlen($html_body);
+ }
+ }
}
// Pass the headers to the mailer, then save the state so we can show
// them in the web UI. If the mail must be encrypted, we remove headers
// which are not on a strict whitelist to avoid disclosing information.
$filtered_headers = $this->filterHeaders($headers, $must_encrypt);
foreach ($filtered_headers as $header) {
list($header_key, $header_value) = $header;
$mailer->addHeader($header_key, $header_value);
}
$this->setParam('headers.unfiltered', $headers);
$this->setParam('headers.sent', $filtered_headers);
// Save the final deliverability outcomes and reasoning so we can
// explain why things happened the way they did.
$actor_list = array();
foreach ($actors as $actor) {
$actor_list[$actor->getPHID()] = array(
'deliverable' => $actor->isDeliverable(),
'reasons' => $actor->getDeliverabilityReasons(),
);
}
$this->setParam('actors.sent', $actor_list);
$this->setParam('routing.sent', $this->getParam('routing'));
$this->setParam('routingmap.sent', $this->getRoutingRuleMap());
if (!$add_to && !$add_cc) {
$this->setMessage(
pht(
'Message has no valid recipients: all To/Cc are disabled, '.
'invalid, or configured not to receive this mail.'));
return null;
}
if ($this->getIsErrorEmail()) {
$all_recipients = array_merge($add_to, $add_cc);
if ($this->shouldRateLimitMail($all_recipients)) {
$this->setMessage(
pht(
'This is an error email, but one or more recipients have '.
'exceeded the error email rate limit. Declining to deliver '.
'message.'));
return null;
}
}
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$this->setMessage(
pht(
'Phabricator is running in silent mode. See `%s` '.
'in the configuration to change this setting.',
'phabricator.silent'));
return null;
}
// Some mailers require a valid "To:" in order to deliver mail. If we
// don't have any "To:", try to fill it in with a placeholder "To:".
// If that also fails, move the "Cc:" line to "To:".
if (!$add_to) {
$placeholder_key = 'metamta.placeholder-to-recipient';
$placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
if ($placeholder !== null) {
$add_to = array($placeholder);
} else {
$add_to = $add_cc;
$add_cc = array();
}
}
$add_to = array_unique($add_to);
$add_cc = array_diff(array_unique($add_cc), $add_to);
$mailer->addTos($add_to);
if ($add_cc) {
$mailer->addCCs($add_cc);
}
return $mailer;
}
private function generateThreadIndex($seed, $is_first_mail) {
// When threading, Outlook ignores the 'References' and 'In-Reply-To'
// headers that most clients use. Instead, it uses a custom 'Thread-Index'
// header. The format of this header is something like this (from
// camel-exchange-folder.c in Evolution Exchange):
/* A new post to a folder gets a 27-byte-long thread index. (The value
* is apparently unique but meaningless.) Each reply to a post gets a
* 32-byte-long thread index whose first 27 bytes are the same as the
* parent's thread index. Each reply to any of those gets a
* 37-byte-long thread index, etc. The Thread-Index header contains a
* base64 representation of this value.
*/
// The specific implementation uses a 27-byte header for the first email
// a recipient receives, and a random 5-byte suffix (32 bytes total)
// thereafter. This means that all the replies are (incorrectly) siblings,
// but it would be very difficult to keep track of the entire tree and this
// gets us reasonable client behavior.
$base = substr(md5($seed), 0, 27);
if (!$is_first_mail) {
// Not totally sure, but it seems like outlook orders replies by
// thread-index rather than timestamp, so to get these to show up in the
// right order we use the time as the last 4 bytes.
$base .= ' '.pack('N', time());
}
return base64_encode($base);
}
public static function shouldMailEachRecipient() {
return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
}
/* -( Managing Recipients )------------------------------------------------ */
/**
* Get all of the recipients for this mail, after preference filters are
* applied. This list has all objects to whom delivery will be attempted.
*
* Note that this expands recipients into their members, because delivery
* is never directly attempted to aggregate actors like projects.
*
* @return list<phid> A list of all recipients to whom delivery will be
* attempted.
* @task recipients
*/
public function buildRecipientList() {
$actors = $this->loadAllActors();
$actors = $this->filterDeliverableActors($actors);
return mpull($actors, 'getPHID');
}
public function loadAllActors() {
$actor_phids = $this->getExpandedRecipientPHIDs();
return $this->loadActors($actor_phids);
}
public function getExpandedRecipientPHIDs() {
$actor_phids = $this->getAllActorPHIDs();
return $this->expandRecipients($actor_phids);
}
private function getAllActorPHIDs() {
return array_merge(
array($this->getParam('from')),
$this->getToPHIDs(),
$this->getCcPHIDs());
}
/**
* Expand a list of recipient PHIDs (possibly including aggregate recipients
* like projects) into a deaggregated list of individual recipient PHIDs.
* For example, this will expand project PHIDs into a list of the project's
* members.
*
* @param list<phid> List of recipient PHIDs, possibly including aggregate
* recipients.
* @return list<phid> Deaggregated list of mailable recipients.
*/
private function expandRecipients(array $phids) {
if ($this->recipientExpansionMap === null) {
$all_phids = $this->getAllActorPHIDs();
$this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
}
$results = array();
foreach ($phids as $phid) {
foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
$results[$recipient_phid] = $recipient_phid;
}
}
return array_keys($results);
}
private function filterDeliverableActors(array $actors) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$deliverable_actors = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable_actors[$phid] = $actor;
}
}
return $deliverable_actors;
}
private function loadActors(array $actor_phids) {
$actor_phids = array_filter($actor_phids);
$viewer = PhabricatorUser::getOmnipotentUser();
$actors = id(new PhabricatorMetaMTAActorQuery())
->setViewer($viewer)
->withPHIDs($actor_phids)
->execute();
if (!$actors) {
return array();
}
if ($this->getForceDelivery()) {
// If we're forcing delivery, skip all the opt-out checks. We don't
// bother annotating reasoning on the mail in this case because it should
// always be obvious why the mail hit this rule (e.g., it is a password
// reset mail).
foreach ($actors as $actor) {
$actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
}
return $actors;
}
// Exclude explicit recipients.
foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
$actor = idx($actors, $phid);
if (!$actor) {
continue;
}
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
}
// Before running more rules, save a list of the actors who were
// deliverable before we started running preference-based rules. This stops
// us from trying to send mail to disabled users just because a Herald rule
// added them, for example.
$deliverable = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable[] = $phid;
}
}
// Exclude muted recipients. We're doing this after saving deliverability
// so that Herald "Send me an email" actions can still punch through a
// mute.
foreach ($this->getMutedPHIDs() as $muted_phid) {
$muted_actor = idx($actors, $muted_phid);
if (!$muted_actor) {
continue;
}
$muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);
}
// For the rest of the rules, order matters. We're going to run all the
// possible rules in order from weakest to strongest, and let the strongest
// matching rule win. The weaker rules leave annotations behind which help
// users understand why the mail was routed the way it was.
// Exclude the actor if their preferences are set.
$from_phid = $this->getParam('from');
$from_actor = idx($actors, $from_phid);
if ($from_actor) {
$from_user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($from_phid))
->needUserSettings(true)
->execute();
$from_user = head($from_user);
if ($from_user) {
$pref_key = PhabricatorEmailSelfActionsSetting::SETTINGKEY;
$exclude_self = $from_user->getUserSetting($pref_key);
if ($exclude_self) {
$from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
}
}
}
$all_prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs($actor_phids)
->needSyntheticPreferences(true)
->execute();
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
$value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
// Exclude all recipients who have set preferences to not receive this type
// of email (for example, a user who says they don't want emails about task
// CC changes).
$tags = $this->getParam('mailtags');
if ($tags) {
foreach ($all_prefs as $phid => $prefs) {
$user_mailtags = $prefs->getSettingValue(
PhabricatorEmailTagsSetting::SETTINGKEY);
// The user must have elected to receive mail for at least one
// of the mailtags.
$send = false;
foreach ($tags as $tag) {
if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
$send = true;
break;
}
}
if (!$send) {
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_MAILTAGS);
}
}
}
foreach ($deliverable as $phid) {
switch ($this->getRoutingRule($phid)) {
case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_ROUTE_AS_NOTIFICATION);
break;
case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
$actors[$phid]->setDeliverable(
PhabricatorMetaMTAActor::REASON_ROUTE_AS_MAIL);
break;
default:
// No change.
break;
}
}
// If recipients were initially deliverable and were added by "Send me an
// email" Herald rules, annotate them as such and make them deliverable
// again, overriding any changes made by the "self mail" and "mail tags"
// settings.
$force_recipients = $this->getForceHeraldMailRecipientPHIDs();
$force_recipients = array_fuse($force_recipients);
if ($force_recipients) {
foreach ($deliverable as $phid) {
if (isset($force_recipients[$phid])) {
$actors[$phid]->setDeliverable(
PhabricatorMetaMTAActor::REASON_FORCE_HERALD);
}
}
}
// Exclude recipients who don't want any mail. This rule is very strong
// and runs last.
foreach ($all_prefs as $phid => $prefs) {
$exclude = $prefs->getSettingValue(
PhabricatorEmailNotificationsSetting::SETTINGKEY);
if ($exclude) {
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
}
}
// Unless delivery was forced earlier (password resets, confirmation mail),
// never send mail to unverified addresses.
foreach ($actors as $phid => $actor) {
if ($actor->getIsVerified()) {
continue;
}
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNVERIFIED);
}
return $actors;
}
private function shouldRateLimitMail(array $all_recipients) {
try {
PhabricatorSystemActionEngine::willTakeAction(
$all_recipients,
new PhabricatorMetaMTAErrorMailAction(),
1);
return false;
} catch (PhabricatorSystemActionRateLimitException $ex) {
return true;
}
}
public function generateHeaders() {
$headers = array();
$headers[] = array('X-Phabricator-Sent-This-Message', 'Yes');
$headers[] = array('X-Mail-Transport-Agent', 'MetaMTA');
// Some clients respect this to suppress OOF and other auto-responses.
$headers[] = array('X-Auto-Response-Suppress', 'All');
$mailtags = $this->getParam('mailtags');
if ($mailtags) {
$tag_header = array();
foreach ($mailtags as $mailtag) {
$tag_header[] = '<'.$mailtag.'>';
}
$tag_header = implode(', ', $tag_header);
$headers[] = array('X-Phabricator-Mail-Tags', $tag_header);
}
$value = $this->getParam('headers', array());
foreach ($value as $pair) {
list($header_key, $header_value) = $pair;
// NOTE: If we have \n in a header, SES rejects the email.
$header_value = str_replace("\n", ' ', $header_value);
$headers[] = array($header_key, $header_value);
}
$is_bulk = $this->getParam('is-bulk');
if ($is_bulk) {
$headers[] = array('Precedence', 'bulk');
}
if ($this->getMustEncrypt()) {
$headers[] = array('X-Phabricator-Must-Encrypt', 'Yes');
}
$related_phid = $this->getRelatedPHID();
if ($related_phid) {
$headers[] = array('Thread-Topic', $related_phid);
}
$headers[] = array('X-Phabricator-Mail-ID', $this->getID());
$unique = Filesystem::readRandomCharacters(16);
$headers[] = array('X-Phabricator-Send-Attempt', $unique);
return $headers;
}
public function getDeliveredHeaders() {
return $this->getParam('headers.sent');
}
public function getUnfilteredHeaders() {
$unfiltered = $this->getParam('headers.unfiltered');
if ($unfiltered === null) {
// Older versions of Phabricator did not filter headers, and thus did
// not record unfiltered headers. If we don't have unfiltered header
// data just return the delivered headers for compatibility.
return $this->getDeliveredHeaders();
}
return $unfiltered;
}
public function getDeliveredActors() {
return $this->getParam('actors.sent');
}
public function getDeliveredRoutingRules() {
return $this->getParam('routing.sent');
}
public function getDeliveredRoutingMap() {
return $this->getParam('routingmap.sent');
}
public function getDeliveredBody() {
return $this->getParam('body.sent');
}
private function filterHeaders(array $headers, $must_encrypt) {
if (!$must_encrypt) {
return $headers;
}
$whitelist = array(
'In-Reply-To',
'Message-ID',
'Precedence',
'References',
'Thread-Index',
'Thread-Topic',
'X-Mail-Transport-Agent',
'X-Auto-Response-Suppress',
'X-Phabricator-Sent-This-Message',
'X-Phabricator-Must-Encrypt',
'X-Phabricator-Mail-ID',
'X-Phabricator-Send-Attempt',
);
// NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags".
// This header contains a significant amount of meaningful information
// about the object.
$whitelist_map = array();
foreach ($whitelist as $term) {
$whitelist_map[phutil_utf8_strtolower($term)] = true;
}
foreach ($headers as $key => $header) {
list($name, $value) = $header;
$name = phutil_utf8_strtolower($name);
if (!isset($whitelist_map[$name])) {
unset($headers[$key]);
}
}
return $headers;
}
public function getURI() {
return '/mail/detail/'.$this->getID().'/';
}
/* -( Routing )------------------------------------------------------------ */
public function addRoutingRule($routing_rule, $phids, $reason_phid) {
$routing = $this->getParam('routing', array());
$routing[] = array(
'routingRule' => $routing_rule,
'phids' => $phids,
'reasonPHID' => $reason_phid,
);
$this->setParam('routing', $routing);
// Throw the routing map away so we rebuild it.
$this->routingMap = null;
return $this;
}
private function getRoutingRule($phid) {
$map = $this->getRoutingRuleMap();
$info = idx($map, $phid, idx($map, 'default'));
if ($info) {
return idx($info, 'rule');
}
return null;
}
private function getRoutingRuleMap() {
if ($this->routingMap === null) {
$map = array();
$routing = $this->getParam('routing', array());
foreach ($routing as $route) {
$phids = $route['phids'];
if ($phids === null) {
$phids = array('default');
}
foreach ($phids as $phid) {
$new_rule = $route['routingRule'];
$current_rule = idx($map, $phid);
if ($current_rule === null) {
$is_stronger = true;
} else {
$is_stronger = PhabricatorMailRoutingRule::isStrongerThan(
$new_rule,
$current_rule);
}
if ($is_stronger) {
$map[$phid] = array(
'rule' => $new_rule,
'reason' => $route['reasonPHID'],
);
}
}
}
$this->routingMap = $map;
}
return $this->routingMap;
}
/* -( Preferences )-------------------------------------------------------- */
private function loadPreferences($target_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
if (self::shouldMailEachRecipient()) {
$preferences = id(new PhabricatorUserPreferencesQuery())
->setViewer($viewer)
->withUserPHIDs(array($target_phid))
->needSyntheticPreferences(true)
->executeOne();
if ($preferences) {
return $preferences;
}
}
return PhabricatorUserPreferences::loadGlobalPreferences($viewer);
}
private function shouldAddRePrefix(PhabricatorUserPreferences $preferences) {
$value = $preferences->getSettingValue(
PhabricatorEmailRePrefixSetting::SETTINGKEY);
return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX);
}
private function shouldVarySubject(PhabricatorUserPreferences $preferences) {
$value = $preferences->getSettingValue(
PhabricatorEmailVarySubjectsSetting::SETTINGKEY);
return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS);
}
private function shouldSendHTML(PhabricatorUserPreferences $preferences) {
$value = $preferences->getSettingValue(
PhabricatorEmailFormatSetting::SETTINGKEY);
return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL);
}
public function shouldRenderMailStampsInBody($viewer) {
$preferences = $this->loadPreferences($viewer->getPHID());
$value = $preferences->getSettingValue(
PhabricatorEmailStampsSetting::SETTINGKEY);
return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$actor_phids = $this->getExpandedRecipientPHIDs();
return in_array($viewer->getPHID(), $actor_phids);
}
public function describeAutomaticCapability($capability) {
return pht(
'The mail sender and message recipients can always see the mail.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$files = $this->loadAttachedFiles($engine->getViewer());
foreach ($files as $file) {
$engine->destroyObject($file);
}
$this->delete();
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMailProperties.php b/src/applications/metamta/storage/PhabricatorMetaMTAMailProperties.php
new file mode 100644
index 000000000..eb454b650
--- /dev/null
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMailProperties.php
@@ -0,0 +1,91 @@
+<?php
+
+final class PhabricatorMetaMTAMailProperties
+ extends PhabricatorMetaMTADAO
+ implements PhabricatorPolicyInterface {
+
+ protected $objectPHID;
+ protected $mailProperties = array();
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'mailProperties' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_object' => array(
+ 'columns' => array('objectPHID'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getMailProperty($key, $default = null) {
+ return idx($this->mailProperties, $key, $default);
+ }
+
+ public function setMailProperty($key, $value) {
+ $this->mailProperties[$key] = $value;
+ return $this;
+ }
+
+ public static function loadMailKey($object) {
+ // If this is an older object with an onboard "mailKey" property, just
+ // use it.
+ // TODO: We should eventually get rid of these and get rid of this
+ // piece of code.
+ if ($object->hasProperty('mailKey')) {
+ return $object->getMailKey();
+ }
+
+ $viewer = PhabricatorUser::getOmnipotentUser();
+ $object_phid = $object->getPHID();
+
+ $properties = id(new PhabricatorMetaMTAMailPropertiesQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object_phid))
+ ->executeOne();
+ if (!$properties) {
+ $properties = id(new self())
+ ->setObjectPHID($object_phid);
+ }
+
+ $mail_key = $properties->getMailProperty('mailKey');
+ if ($mail_key !== null) {
+ return $mail_key;
+ }
+
+ $mail_key = Filesystem::readRandomCharacters(20);
+ $properties->setMailProperty('mailKey', $mail_key);
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ $properties->save();
+ unset($unguarded);
+
+ return $mail_key;
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return PhabricatorPolicies::POLICY_NOONE;
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php
index 25984a2c1..fb48925ed 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php
@@ -1,131 +1,200 @@
<?php
final class PhabricatorMailConfigTestCase
extends PhabricatorTestCase {
public function testMailerPriorities() {
$mailers = $this->newMailersWithConfig(
array(
array(
'key' => 'A',
'type' => 'test',
),
array(
'key' => 'B',
'type' => 'test',
),
));
$this->assertEqual(
array('A', 'B'),
mpull($mailers, 'getKey'));
$mailers = $this->newMailersWithConfig(
array(
array(
'key' => 'A',
'priority' => 1,
'type' => 'test',
),
array(
'key' => 'B',
'priority' => 2,
'type' => 'test',
),
));
$this->assertEqual(
array('B', 'A'),
mpull($mailers, 'getKey'));
$mailers = $this->newMailersWithConfig(
array(
array(
'key' => 'A1',
'priority' => 300,
'type' => 'test',
),
array(
'key' => 'A2',
'priority' => 300,
'type' => 'test',
),
array(
'key' => 'B',
'type' => 'test',
),
array(
'key' => 'C',
'priority' => 400,
'type' => 'test',
),
array(
'key' => 'D',
'type' => 'test',
),
));
// The "A" servers should be shuffled randomly, so either outcome is
// acceptable.
$option_1 = array('C', 'A1', 'A2', 'B', 'D');
$option_2 = array('C', 'A2', 'A1', 'B', 'D');
$actual = mpull($mailers, 'getKey');
$this->assertTrue(($actual === $option_1) || ($actual === $option_2));
// Make sure that when we're load balancing we actually send traffic to
// both servers reasonably often.
$saw_a1 = false;
$saw_a2 = false;
$attempts = 0;
while (true) {
$mailers = $this->newMailersWithConfig(
array(
array(
'key' => 'A1',
'priority' => 300,
'type' => 'test',
),
array(
'key' => 'A2',
'priority' => 300,
'type' => 'test',
),
));
$first_key = head($mailers)->getKey();
if ($first_key == 'A1') {
$saw_a1 = true;
}
if ($first_key == 'A2') {
$saw_a2 = true;
}
if ($saw_a1 && $saw_a2) {
break;
}
if ($attempts++ > 1024) {
throw new Exception(
pht(
'Load balancing between two mail servers did not select both '.
'servers after an absurd number of attempts.'));
}
}
$this->assertTrue($saw_a1 && $saw_a2);
}
- private function newMailersWithConfig(array $config) {
+ public function testMailerConstraints() {
+ $config = array(
+ array(
+ 'key' => 'X1',
+ 'type' => 'test',
+ ),
+ array(
+ 'key' => 'X1-in',
+ 'type' => 'test',
+ 'outbound' => false,
+ ),
+ array(
+ 'key' => 'X1-out',
+ 'type' => 'test',
+ 'inbound' => false,
+ ),
+ array(
+ 'key' => 'X1-void',
+ 'type' => 'test',
+ 'inbound' => false,
+ 'outbound' => false,
+ ),
+ );
+
+ $mailers = $this->newMailersWithConfig(
+ $config,
+ array());
+ $this->assertEqual(4, count($mailers));
+
+ $mailers = $this->newMailersWithConfig(
+ $config,
+ array(
+ 'inbound' => true,
+ ));
+ $this->assertEqual(2, count($mailers));
+
+ $mailers = $this->newMailersWithConfig(
+ $config,
+ array(
+ 'outbound' => true,
+ ));
+ $this->assertEqual(2, count($mailers));
+
+ $mailers = $this->newMailersWithConfig(
+ $config,
+ array(
+ 'inbound' => true,
+ 'outbound' => true,
+ ));
+ $this->assertEqual(1, count($mailers));
+
+ $mailers = $this->newMailersWithConfig(
+ $config,
+ array(
+ 'types' => array('test'),
+ ));
+ $this->assertEqual(4, count($mailers));
+
+ $mailers = $this->newMailersWithConfig(
+ $config,
+ array(
+ 'types' => array('duck'),
+ ));
+ $this->assertEqual(0, count($mailers));
+ }
+
+ private function newMailersWithConfig(
+ array $config,
+ array $constraints = array()) {
+
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('cluster.mailers', $config);
- $mailers = PhabricatorMetaMTAMail::newMailers();
+ $mailers = PhabricatorMetaMTAMail::newMailers($constraints);
unset($env);
return $mailers;
}
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
index c0045301f..d20a28fc1 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
@@ -1,334 +1,414 @@
<?php
final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testMailSendFailures() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
// Normally, the send should succeed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail->sendWithMailers(array($mailer));
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_SENT,
$mail->getStatus());
// When the mailer fails temporarily, the mail should remain queued.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->setFailTemporarily(true);
try {
$mail->sendWithMailers(array($mailer));
} catch (Exception $ex) {
// Ignore.
}
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_QUEUE,
$mail->getStatus());
// When the mailer fails permanently, the mail should be failed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->setFailPermanently(true);
try {
$mail->sendWithMailers(array($mailer));
} catch (Exception $ex) {
// Ignore.
}
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_FAIL,
$mail->getStatus());
}
public function testRecipients() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"To" is a recipient.'));
// Test that the "No Self Mail" and "No Mail" preferences work correctly.
$mail->setFrom($phid);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" does not exclude recipients by default.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailSelfActionsSetting::SETTINGKEY,
true);
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('"From" excludes recipients with no-self-mail set.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailSelfActionsSetting::SETTINGKEY,
null);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" does not exclude recipients by default.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailNotificationsSetting::SETTINGKEY,
true);
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('"From" excludes recipients with no-mail set.'));
$mail->setForceDelivery(true);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" includes no-mail recipients when forced.'));
$mail->setForceDelivery(false);
$user = $this->writeSetting(
$user,
PhabricatorEmailNotificationsSetting::SETTINGKEY,
null);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" does not exclude recipients by default.'));
// Test that explicit exclusion works correctly.
$mail->setExcludeMailRecipientPHIDs(array($phid));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('Explicit exclude excludes recipients.'));
$mail->setExcludeMailRecipientPHIDs(array());
// Test that mail tag preferences exclude recipients.
$user = $this->writeSetting(
$user,
PhabricatorEmailTagsSetting::SETTINGKEY,
array(
'test-tag' => false,
));
$mail->setMailTags(array('test-tag'));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('Tag preference excludes recipients.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailTagsSetting::SETTINGKEY,
null);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'Recipients restored after tag preference removed.');
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND isPrimary = 1',
$phid);
$email->setIsVerified(0)->save();
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('Mail not sent to unverified address.'));
$email->setIsVerified(1)->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('Mail sent to verified address.'));
}
public function testThreadIDHeaders() {
$this->runThreadIDHeadersWithConfiguration(true, true);
$this->runThreadIDHeadersWithConfiguration(true, false);
$this->runThreadIDHeadersWithConfiguration(false, true);
$this->runThreadIDHeadersWithConfiguration(false, false);
}
private function runThreadIDHeadersWithConfiguration(
$supports_message_id,
$is_first_mail) {
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->prepareForSend(
array(
'supportsMessageIDHeader' => $supports_message_id,
));
$thread_id = '<somethread-12345@somedomain.tld>';
$mail = new PhabricatorMetaMTAMail();
$mail->setThreadID($thread_id, $is_first_mail);
$mail->sendWithMailers(array($mailer));
$guts = $mailer->getGuts();
$dict = ipull($guts['headers'], 1, 0);
if ($is_first_mail && $supports_message_id) {
$expect_message_id = true;
$expect_in_reply_to = false;
$expect_references = false;
} else {
$expect_message_id = false;
$expect_in_reply_to = true;
$expect_references = true;
}
$case = '<message-id = '.($supports_message_id ? 'Y' : 'N').', '.
'first = '.($is_first_mail ? 'Y' : 'N').'>';
$this->assertTrue(
isset($dict['Thread-Index']),
pht('Expect Thread-Index header for case %s.', $case));
$this->assertEqual(
$expect_message_id,
isset($dict['Message-ID']),
pht(
'Expectation about existence of Message-ID header for case %s.',
$case));
$this->assertEqual(
$expect_in_reply_to,
isset($dict['In-Reply-To']),
pht(
'Expectation about existence of In-Reply-To header for case %s.',
$case));
$this->assertEqual(
$expect_references,
isset($dict['References']),
pht(
'Expectation about existence of References header for case %s.',
$case));
}
private function writeSetting(PhabricatorUser $user, $key, $value) {
$preferences = PhabricatorUserPreferences::loadUserPreferences($user);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($key, $value);
$editor->applyTransactions($preferences, $xactions);
return id(new PhabricatorPeopleQuery())
->setViewer($user)
->withIDs(array($user->getID()))
->executeOne();
}
public function testMailerFailover() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$status_sent = PhabricatorMailOutboundStatus::STATUS_SENT;
$status_queue = PhabricatorMailOutboundStatus::STATUS_QUEUE;
$status_fail = PhabricatorMailOutboundStatus::STATUS_FAIL;
$mailer1 = id(new PhabricatorMailImplementationTestAdapter())
->setKey('mailer1');
$mailer2 = id(new PhabricatorMailImplementationTestAdapter())
->setKey('mailer2');
$mailers = array(
$mailer1,
$mailer2,
);
// Send mail with both mailers active. The first mailer should be used.
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->sendWithMailers($mailers);
$this->assertEqual($status_sent, $mail->getStatus());
$this->assertEqual('mailer1', $mail->getMailerKey());
// If the first mailer fails, the mail should be sent with the second
// mailer. Since we transmitted the mail, this doesn't raise an exception.
$mailer1->setFailTemporarily(true);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->sendWithMailers($mailers);
$this->assertEqual($status_sent, $mail->getStatus());
$this->assertEqual('mailer2', $mail->getMailerKey());
// If both mailers fail, the mail should remain in queue.
$mailer2->setFailTemporarily(true);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid));
$caught = null;
try {
$mail->sendWithMailers($mailers);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
$this->assertEqual($status_queue, $mail->getStatus());
$this->assertEqual(null, $mail->getMailerKey());
$mailer1->setFailTemporarily(false);
$mailer2->setFailTemporarily(false);
// If the first mailer fails permanently, the mail should fail even though
// the second mailer isn't configured to fail.
$mailer1->setFailPermanently(true);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid));
$caught = null;
try {
$mail->sendWithMailers($mailers);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
$this->assertEqual($status_fail, $mail->getStatus());
$this->assertEqual(null, $mail->getMailerKey());
}
+ public function testMailSizeLimits() {
+ $env = PhabricatorEnv::beginScopedEnv();
+ $env->overrideEnvConfig('metamta.email-body-limit', 1024 * 512);
+
+ $user = $this->generateNewTestUser();
+ $phid = $user->getPHID();
+
+ $string_1kb = str_repeat('x', 1024);
+ $html_1kb = str_repeat('y', 1024);
+ $string_1mb = str_repeat('x', 1024 * 1024);
+ $html_1mb = str_repeat('y', 1024 * 1024);
+
+ // First, send a mail with a small text body and a small HTML body to make
+ // sure the basics work properly.
+ $mail = id(new PhabricatorMetaMTAMail())
+ ->addTos(array($phid))
+ ->setBody($string_1kb)
+ ->setHTMLBody($html_1kb);
+
+ $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mail->sendWithMailers(array($mailer));
+ $this->assertEqual(
+ PhabricatorMailOutboundStatus::STATUS_SENT,
+ $mail->getStatus());
+
+ $text_body = $mailer->getBody();
+ $html_body = $mailer->getHTMLBody();
+
+ $this->assertEqual($string_1kb, $text_body);
+ $this->assertEqual($html_1kb, $html_body);
+
+
+ // Now, send a mail with a large text body and a large HTML body. We expect
+ // the text body to be truncated and the HTML body to be dropped.
+ $mail = id(new PhabricatorMetaMTAMail())
+ ->addTos(array($phid))
+ ->setBody($string_1mb)
+ ->setHTMLBody($html_1mb);
+
+ $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mail->sendWithMailers(array($mailer));
+ $this->assertEqual(
+ PhabricatorMailOutboundStatus::STATUS_SENT,
+ $mail->getStatus());
+
+ $text_body = $mailer->getBody();
+ $html_body = $mailer->getHTMLBody();
+
+ // We expect the body was truncated, because it exceeded the body limit.
+ $this->assertTrue(
+ (strlen($text_body) < strlen($string_1mb)),
+ pht('Text Body Truncated'));
+
+ // We expect the HTML body was dropped completely after the text body was
+ // truncated.
+ $this->assertTrue(
+ !strlen($html_body),
+ pht('HTML Body Removed'));
+
+
+ // Next send a mail with a small text body and a large HTML body. We expect
+ // the text body to be intact and the HTML body to be dropped.
+ $mail = id(new PhabricatorMetaMTAMail())
+ ->addTos(array($phid))
+ ->setBody($string_1kb)
+ ->setHTMLBody($html_1mb);
+
+ $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mail->sendWithMailers(array($mailer));
+ $this->assertEqual(
+ PhabricatorMailOutboundStatus::STATUS_SENT,
+ $mail->getStatus());
+
+ $text_body = $mailer->getBody();
+ $html_body = $mailer->getHTMLBody();
+
+ $this->assertEqual($string_1kb, $text_body);
+ $this->assertTrue(!strlen($html_body));
+ }
+
}
diff --git a/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php b/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php
index 68cb0e79b..4b231b959 100644
--- a/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php
+++ b/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php
@@ -1,27 +1,29 @@
<?php
final class PhabricatorMetaMTAMailableFunctionDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Subscribers');
}
public function getPlaceholderText() {
- return pht('Type a username, project, mailing list, or function...');
+ return pht(
+ 'Type a username, project, mailing list, package, or function...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorMetaMTAApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorViewerDatasource(),
new PhabricatorPeopleDatasource(),
new PhabricatorProjectMembersDatasource(),
new PhabricatorProjectDatasource(),
+ new PhabricatorOwnersPackageDatasource(),
);
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php
index 3e30ead9e..1e956a60e 100644
--- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php
@@ -1,83 +1,163 @@
<?php
final class PhabricatorNotificationPanelController
extends PhabricatorNotificationController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
+ $unread_count = $viewer->getUnreadNotificationCount();
+
+ $warning = $this->prunePhantomNotifications($unread_count);
+
$query = id(new PhabricatorNotificationQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->setLimit(10);
$stories = $query->execute();
$clear_ui_class = 'phabricator-notification-clear-all';
$clear_uri = id(new PhutilURI('/notification/clear/'));
if ($stories) {
$builder = id(new PhabricatorNotificationBuilder($stories))
->setUser($viewer);
$notifications_view = $builder->buildView();
$content = $notifications_view->render();
$clear_uri->setQueryParam(
'chronoKey',
head($stories)->getChronologicalKey());
} else {
$content = phutil_tag_div(
'phabricator-notification no-notifications',
pht('You have no notifications.'));
$clear_ui_class .= ' disabled';
}
$clear_ui = javelin_tag(
'a',
array(
'sigil' => 'workflow',
'href' => (string)$clear_uri,
'class' => $clear_ui_class,
),
pht('Mark All Read'));
$notifications_link = phutil_tag(
'a',
array(
'href' => '/notification/',
),
pht('Notifications'));
$connection_status = new PhabricatorNotificationStatusView();
$connection_ui = phutil_tag(
'div',
array(
'class' => 'phabricator-notification-footer',
),
$connection_status);
$header = phutil_tag(
'div',
array(
'class' => 'phabricator-notification-header',
),
array(
$notifications_link,
$clear_ui,
));
$content = hsprintf(
- '%s%s%s',
+ '%s%s%s%s',
$header,
+ $warning,
$content,
$connection_ui);
- $unread_count = $viewer->getUnreadNotificationCount();
-
$json = array(
'content' => $content,
'number' => (int)$unread_count,
);
return id(new AphrontAjaxResponse())->setContent($json);
}
+
+ private function prunePhantomNotifications($unread_count) {
+ // See T8953. If you have an unread notification about an object you
+ // do not have permission to view, it isn't possible to clear it by
+ // visiting the object. Identify these notifications and mark them as
+ // read.
+
+ $viewer = $this->getViewer();
+
+ if (!$unread_count) {
+ return null;
+ }
+
+ $table = new PhabricatorFeedStoryNotification();
+ $conn = $table->establishConnection('r');
+
+ $rows = queryfx_all(
+ $conn,
+ 'SELECT chronologicalKey, primaryObjectPHID FROM %T
+ WHERE userPHID = %s AND hasViewed = 0',
+ $table->getTableName(),
+ $viewer->getPHID());
+ if (!$rows) {
+ return null;
+ }
+
+ $map = array();
+ foreach ($rows as $row) {
+ $map[$row['primaryObjectPHID']][] = $row['chronologicalKey'];
+ }
+
+ $handles = $viewer->loadHandles(array_keys($map));
+ $purge_keys = array();
+ foreach ($handles as $handle) {
+ $phid = $handle->getPHID();
+ if ($handle->isComplete()) {
+ continue;
+ }
+
+ foreach ($map[$phid] as $chronological_key) {
+ $purge_keys[] = $chronological_key;
+ }
+ }
+
+ if (!$purge_keys) {
+ return null;
+ }
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
+ $conn = $table->establishConnection('w');
+ queryfx(
+ $conn,
+ 'UPDATE %T SET hasViewed = 1
+ WHERE userPHID = %s AND chronologicalKey IN (%Ls)',
+ $table->getTableName(),
+ $viewer->getPHID(),
+ $purge_keys);
+
+ PhabricatorUserCache::clearCache(
+ PhabricatorUserNotificationCountCacheType::KEY_COUNT,
+ $viewer->getPHID());
+
+ unset($unguarded);
+
+ return phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phabricator-notification phabricator-notification-warning',
+ ),
+ pht(
+ '%s notification(s) about objects which no longer exist or which '.
+ 'you can no longer see were discarded.',
+ phutil_count($purge_keys)));
+ }
+
+
}
diff --git a/src/applications/notification/query/PhabricatorNotificationQuery.php b/src/applications/notification/query/PhabricatorNotificationQuery.php
index aa430a251..b40bdee06 100644
--- a/src/applications/notification/query/PhabricatorNotificationQuery.php
+++ b/src/applications/notification/query/PhabricatorNotificationQuery.php
@@ -1,114 +1,114 @@
<?php
/**
* @task config Configuring the Query
* @task exec Query Execution
*/
final class PhabricatorNotificationQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $userPHIDs;
private $keys;
private $unread;
/* -( Configuring the Query )---------------------------------------------- */
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withKeys(array $keys) {
$this->keys = $keys;
return $this;
}
/**
* Filter results by read/unread status. Note that `true` means to return
* only unread notifications, while `false` means to return only //read//
* notifications. The default is `null`, which returns both.
*
* @param mixed True or false to filter results by read status. Null to remove
* the filter.
* @return this
* @task config
*/
public function withUnread($unread) {
$this->unread = $unread;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
protected function loadPage() {
$story_table = new PhabricatorFeedStoryData();
$notification_table = new PhabricatorFeedStoryNotification();
$conn = $story_table->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT story.*, notif.hasViewed FROM %T notif
JOIN %T story ON notif.chronologicalKey = story.chronologicalKey
%Q
ORDER BY notif.chronologicalKey DESC
%Q',
$notification_table->getTableName(),
$story_table->getTableName(),
$this->buildWhereClause($conn),
$this->buildLimitClause($conn));
$viewed_map = ipull($data, 'hasViewed', 'chronologicalKey');
$stories = PhabricatorFeedStory::loadAllFromRows(
$data,
$this->getViewer());
foreach ($stories as $key => $story) {
$story->setHasViewed($viewed_map[$key]);
}
return $stories;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'notif.userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->unread !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'notif.hasViewed = %d',
(int)!$this->unread);
}
if ($this->keys) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'notif.chronologicalKey IN (%Ls)',
$this->keys);
}
- return $this->formatWhereClause($where);
+ return $where;
}
protected function getResultCursor($item) {
return $item->getChronologicalKey();
}
public function getQueryApplicationClass() {
return 'PhabricatorNotificationsApplication';
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
index c7e96594f..1caf12a82 100644
--- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
@@ -1,342 +1,352 @@
<?php
final class PhabricatorOwnersDetailController
extends PhabricatorOwnersController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$package = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needPaths(true)
->executeOne();
if (!$package) {
return new Aphront404Response();
}
$paths = $package->getPaths();
$repository_phids = array();
foreach ($paths as $path) {
$repository_phids[$path->getRepositoryPHID()] = true;
}
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array_keys($repository_phids))
->execute();
$repositories = mpull($repositories, null, 'getPHID');
} else {
$repositories = array();
}
$field_list = PhabricatorCustomField::getObjectFields(
$package,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($package);
$curtain = $this->buildCurtain($package);
$details = $this->buildPackageDetailView($package, $field_list);
if ($package->isArchived()) {
$header_icon = 'fa-ban';
$header_name = pht('Archived');
$header_color = 'dark';
} else {
$header_icon = 'fa-check';
$header_name = pht('Active');
$header_color = 'bluegrey';
}
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($package->getName())
->setStatus($header_icon, $header_color, $header_name)
->setPolicyObject($package)
->setHeaderIcon('fa-gift');
$commit_views = array();
$commit_uri = id(new PhutilURI('/diffusion/commit/'))
->setQueryParams(
array(
'package' => $package->getPHID(),
));
$status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
$attention_commits = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withPackagePHIDs(array($package->getPHID()))
->withStatuses(
array(
$status_concern,
))
->needCommitData(true)
->needAuditRequests(true)
->setLimit(10)
->execute();
$view = id(new PhabricatorAuditListView())
->setUser($viewer)
->setNoDataString(pht('This package has no open problem commits.'))
->setCommits($attention_commits);
$commit_views[] = array(
'view' => $view,
'header' => pht('Needs Attention'),
'icon' => 'fa-warning',
'button' => id(new PHUIButtonView())
->setTag('a')
->setHref($commit_uri->alter('status', $status_concern))
->setIcon('fa-list-ul')
->setText(pht('View All')),
);
$all_commits = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withPackagePHIDs(array($package->getPHID()))
->needCommitData(true)
->needAuditRequests(true)
->setLimit(25)
->execute();
$view = id(new PhabricatorAuditListView())
->setUser($viewer)
->setCommits($all_commits)
->setNoDataString(pht('No commits in this package.'));
$commit_views[] = array(
'view' => $view,
'header' => pht('Recent Commits'),
'icon' => 'fa-code',
'button' => id(new PHUIButtonView())
->setTag('a')
->setHref($commit_uri)
->setIcon('fa-list-ul')
->setText(pht('View All')),
);
$commit_panels = array();
foreach ($commit_views as $commit_view) {
$commit_panel = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$commit_header = id(new PHUIHeaderView())
->setHeader($commit_view['header'])
->setHeaderIcon($commit_view['icon']);
if (isset($commit_view['button'])) {
$commit_header->addActionLink($commit_view['button']);
}
$commit_panel->setHeader($commit_header);
$commit_panel->appendChild($commit_view['view']);
$commit_panels[] = $commit_panel;
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($package->getMonogram());
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$package,
new PhabricatorOwnersPackageTransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$this->renderPathsTable($paths, $repositories),
$commit_panels,
$timeline,
))
->addPropertySection(pht('Details'), $details);
return $this->newPage()
->setTitle($package->getName())
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildPackageDetailView(
PhabricatorOwnersPackage $package,
PhabricatorCustomFieldList $field_list) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$owners = $package->getOwners();
if ($owners) {
$owner_list = $viewer->renderHandleList(mpull($owners, 'getUserPHID'));
} else {
$owner_list = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Owners'), $owner_list);
$dominion = $package->getDominion();
$dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
$spec = idx($dominion_map, $dominion, array());
$name = idx($spec, 'short', $dominion);
$view->addProperty(pht('Dominion'), $name);
$auto = $package->getAutoReview();
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$spec = idx($autoreview_map, $auto, array());
$name = idx($spec, 'name', $auto);
$view->addProperty(pht('Auto Review'), $name);
if ($package->getAuditingEnabled()) {
$auditing = pht('Enabled');
} else {
$auditing = pht('Disabled');
}
$view->addProperty(pht('Auditing'), $auditing);
+ $ignored = $package->getIgnoredPathAttributes();
+ $ignored = array_keys($ignored);
+ if ($ignored) {
+ $ignored = implode(', ', $ignored);
+ } else {
+ $ignored = phutil_tag('em', array(), pht('None'));
+ }
+
+ $view->addProperty(pht('Ignored Attributes'), $ignored);
+
$description = $package->getDescription();
if (strlen($description)) {
$description = new PHUIRemarkupView($viewer, $description);
$view->addSectionHeader(pht('Description'));
$view->addTextContent($description);
}
$field_list->appendFieldsToPropertyList(
$package,
$viewer,
$view);
return $view;
}
private function buildCurtain(PhabricatorOwnersPackage $package) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$package,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $package->getID();
$edit_uri = $this->getApplicationURI("/edit/{$id}/");
$paths_uri = $this->getApplicationURI("/paths/{$id}/");
$curtain = $this->newCurtainView($package);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Package'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
if ($package->isArchived()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Activate Package'))
->setIcon('fa-check')
->setDisabled(!$can_edit)
->setWorkflow($can_edit)
->setHref($this->getApplicationURI("/archive/{$id}/")));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Archive Package'))
->setIcon('fa-ban')
->setDisabled(!$can_edit)
->setWorkflow($can_edit)
->setHref($this->getApplicationURI("/archive/{$id}/")));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Paths'))
->setIcon('fa-folder-open')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($paths_uri));
return $curtain;
}
private function renderPathsTable(array $paths, array $repositories) {
$viewer = $this->getViewer();
$rows = array();
foreach ($paths as $path) {
$repo = idx($repositories, $path->getRepositoryPHID());
if (!$repo) {
continue;
}
$href = $repo->generateURI(
array(
'branch' => $repo->getDefaultBranch(),
'path' => $path->getPathDisplay(),
'action' => 'browse',
));
$path_link = phutil_tag(
'a',
array(
'href' => (string)$href,
),
$path->getPathDisplay());
$rows[] = array(
($path->getExcluded() ? '-' : '+'),
$repo->getName(),
$path_link,
);
}
$info = null;
if (!$paths) {
$info = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'This package does not contain any paths yet. Use '.
'"Edit Paths" to add some.'),
));
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Repository'),
pht('Path'),
))
->setColumnClasses(
array(
null,
null,
'wide',
));
if ($info) {
$table->setNotice($info);
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Paths'))
->setHeaderIcon('fa-folder-open');
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
return $box;
}
}
diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
index 4adaebc58..d2eee5685 100644
--- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
+++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
@@ -1,179 +1,190 @@
<?php
final class PhabricatorOwnersPackageEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'owners.package';
public function getEngineName() {
return pht('Owners Packages');
}
public function getSummaryHeader() {
return pht('Configure Owners Package Forms');
}
public function getSummaryText() {
return pht('Configure forms for creating and editing packages in Owners.');
}
public function getEngineApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function newEditableObject() {
return PhabricatorOwnersPackage::initializeNewPackage($this->getViewer());
}
protected function newObjectQuery() {
return id(new PhabricatorOwnersPackageQuery())
->needPaths(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Package');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Package: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return pht('Package %d', $object->getID());
}
protected function getObjectCreateShortText() {
return pht('Create Package');
}
protected function getObjectName() {
return pht('Package');
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function buildCustomEditFields($object) {
$paths_help = pht(<<<EOTEXT
When updating the paths for a package, pass a list of dictionaries like
this as the `value` for the transaction:
```lang=json, name="Example Paths Value"
[
{
"repositoryPHID": "PHID-REPO-1234",
"path": "/path/to/directory/",
"excluded": false
},
{
"repositoryPHID": "PHID-REPO-1234",
"path": "/another/example/path/",
"excluded": false
}
]
```
This transaction will set the paths to the list you provide, overwriting any
previous paths.
Generally, you will call `owners.search` first to get a list of current paths
(which are provided in the same format), make changes, then update them by
applying a transaction of this type.
EOTEXT
);
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$autoreview_map = ipull($autoreview_map, 'name');
$dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
$dominion_map = ipull($dominion_map, 'name');
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setDescription(pht('Name of the package.'))
->setTransactionType(
PhabricatorOwnersPackageNameTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getName()),
id(new PhabricatorDatasourceEditField())
->setKey('owners')
->setLabel(pht('Owners'))
->setDescription(pht('Users and projects which own the package.'))
->setTransactionType(
PhabricatorOwnersPackageOwnersTransaction::TRANSACTIONTYPE)
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setIsCopyable(true)
->setValue($object->getOwnerPHIDs()),
id(new PhabricatorSelectEditField())
->setKey('dominion')
->setLabel(pht('Dominion'))
->setDescription(
pht('Change package dominion rules.'))
->setTransactionType(
PhabricatorOwnersPackageDominionTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getDominion())
->setOptions($dominion_map),
id(new PhabricatorSelectEditField())
->setKey('autoReview')
->setLabel(pht('Auto Review'))
->setDescription(
pht(
'Automatically trigger reviews for commits affecting files in '.
'this package.'))
->setTransactionType(
PhabricatorOwnersPackageAutoreviewTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getAutoReview())
->setOptions($autoreview_map),
id(new PhabricatorSelectEditField())
->setKey('auditing')
->setLabel(pht('Auditing'))
->setDescription(
pht(
'Automatically trigger audits for commits affecting files in '.
'this package.'))
->setTransactionType(
PhabricatorOwnersPackageAuditingTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getAuditingEnabled())
->setOptions(
array(
'' => pht('Disabled'),
'1' => pht('Enabled'),
)),
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Human-readable description of the package.'))
->setTransactionType(
PhabricatorOwnersPackageDescriptionTransaction::TRANSACTIONTYPE)
->setValue($object->getDescription()),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setDescription(pht('Archive or enable the package.'))
->setTransactionType(
PhabricatorOwnersPackageStatusTransaction::TRANSACTIONTYPE)
->setIsConduitOnly(true)
->setValue($object->getStatus())
->setOptions($object->getStatusNameMap()),
+ id(new PhabricatorCheckboxesEditField())
+ ->setKey('ignored')
+ ->setLabel(pht('Ignored Attributes'))
+ ->setDescription(pht('Ignore paths with any of these attributes.'))
+ ->setTransactionType(
+ PhabricatorOwnersPackageIgnoredTransaction::TRANSACTIONTYPE)
+ ->setValue(array_keys($object->getIgnoredPathAttributes()))
+ ->setOptions(
+ array(
+ 'generated' => pht('Ignore generated files (review only).'),
+ )),
id(new PhabricatorConduitEditField())
->setKey('paths.set')
->setLabel(pht('Paths'))
->setIsConduitOnly(true)
->setTransactionType(
PhabricatorOwnersPackagePathsTransaction::TRANSACTIONTYPE)
->setConduitDescription(
pht('Overwrite existing package paths with new paths.'))
->setConduitTypeDescription(
pht('List of dictionaries, each describing a path.'))
->setConduitDocumentation($paths_help),
);
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php
index c76864702..7e2a348c8 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPackage.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php
@@ -1,715 +1,815 @@
<?php
final class PhabricatorOwnersPackage
extends PhabricatorOwnersDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorNgramsInterface {
protected $name;
protected $auditingEnabled;
protected $autoReview;
protected $description;
- protected $mailKey;
protected $status;
protected $viewPolicy;
protected $editPolicy;
protected $dominion;
+ protected $properties = array();
private $paths = self::ATTACHABLE;
private $owners = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $pathRepositoryMap = array();
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
const AUTOREVIEW_NONE = 'none';
const AUTOREVIEW_SUBSCRIBE = 'subscribe';
const AUTOREVIEW_SUBSCRIBE_ALWAYS = 'subscribe-always';
const AUTOREVIEW_REVIEW = 'review';
const AUTOREVIEW_REVIEW_ALWAYS = 'review-always';
const AUTOREVIEW_BLOCK = 'block';
const AUTOREVIEW_BLOCK_ALWAYS = 'block-always';
const DOMINION_STRONG = 'strong';
const DOMINION_WEAK = 'weak';
+ const PROPERTY_IGNORED = 'ignored';
+
public static function initializeNewPackage(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorOwnersApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorOwnersDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
PhabricatorOwnersDefaultEditCapability::CAPABILITY);
return id(new PhabricatorOwnersPackage())
->setAuditingEnabled(0)
->setAutoReview(self::AUTOREVIEW_NONE)
->setDominion(self::DOMINION_STRONG)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->attachPaths(array())
->setStatus(self::STATUS_ACTIVE)
->attachOwners(array())
->setDescription('');
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
public static function getAutoreviewOptionsMap() {
return array(
self::AUTOREVIEW_NONE => array(
'name' => pht('No Autoreview'),
),
self::AUTOREVIEW_REVIEW => array(
'name' => pht('Review Changes With Non-Owner Author'),
'authority' => true,
),
self::AUTOREVIEW_BLOCK => array(
'name' => pht('Review Changes With Non-Owner Author (Blocking)'),
'authority' => true,
),
self::AUTOREVIEW_SUBSCRIBE => array(
'name' => pht('Subscribe to Changes With Non-Owner Author'),
'authority' => true,
),
self::AUTOREVIEW_REVIEW_ALWAYS => array(
'name' => pht('Review All Changes'),
),
self::AUTOREVIEW_BLOCK_ALWAYS => array(
'name' => pht('Review All Changes (Blocking)'),
),
self::AUTOREVIEW_SUBSCRIBE_ALWAYS => array(
'name' => pht('Subscribe to All Changes'),
),
);
}
public static function getDominionOptionsMap() {
return array(
self::DOMINION_STRONG => array(
'name' => pht('Strong (Control All Paths)'),
'short' => pht('Strong'),
),
self::DOMINION_WEAK => array(
'name' => pht('Weak (Control Unowned Paths)'),
'short' => pht('Weak'),
),
);
}
protected function getConfiguration() {
return array(
// This information is better available from the history table.
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort',
'description' => 'text',
'auditingEnabled' => 'bool',
- 'mailKey' => 'bytes20',
'status' => 'text32',
'autoReview' => 'text32',
'dominion' => 'text32',
),
) + parent::getConfiguration();
}
- public function generatePHID() {
- return PhabricatorPHID::generateNewPHID(
- PhabricatorOwnersPackagePHIDType::TYPECONST);
+ public function getPHIDType() {
+ return PhabricatorOwnersPackagePHIDType::TYPECONST;
}
- public function save() {
- if (!$this->getMailKey()) {
- $this->setMailKey(Filesystem::readRandomCharacters(20));
- }
+ public function isArchived() {
+ return ($this->getStatus() == self::STATUS_ARCHIVED);
+ }
- return parent::save();
+ public function getMustMatchUngeneratedPaths() {
+ $ignore_attributes = $this->getIgnoredPathAttributes();
+ return !empty($ignore_attributes['generated']);
}
- public function isArchived() {
- return ($this->getStatus() == self::STATUS_ARCHIVED);
+ public function getPackageProperty($key, $default = null) {
+ return idx($this->properties, $key, $default);
}
- public function setName($name) {
- $this->name = $name;
+ public function setPackageProperty($key, $value) {
+ $this->properties[$key] = $value;
return $this;
}
+ public function getIgnoredPathAttributes() {
+ return $this->getPackageProperty(self::PROPERTY_IGNORED, array());
+ }
+
+ public function setIgnoredPathAttributes(array $attributes) {
+ return $this->setPackageProperty(self::PROPERTY_IGNORED, $attributes);
+ }
+
public function loadOwners() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public function loadPaths() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public static function loadAffectedPackages(
PhabricatorRepository $repository,
array $paths) {
if (!$paths) {
return array();
}
return self::loadPackagesForPaths($repository, $paths);
}
+ public static function loadAffectedPackagesForChangesets(
+ PhabricatorRepository $repository,
+ DifferentialDiff $diff,
+ array $changesets) {
+ assert_instances_of($changesets, 'DifferentialChangeset');
+
+ $paths_all = array();
+ $paths_ungenerated = array();
+
+ foreach ($changesets as $changeset) {
+ $path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
+
+ $paths_all[] = $path;
+
+ if (!$changeset->isGeneratedChangeset()) {
+ $paths_ungenerated[] = $path;
+ }
+ }
+
+ if (!$paths_all) {
+ return array();
+ }
+
+ $packages_all = self::loadAffectedPackages(
+ $repository,
+ $paths_all);
+
+ // If there are no generated changesets, we can't possibly need to throw
+ // away any packages for matching only generated paths. Just return the
+ // full set of packages.
+ if ($paths_ungenerated === $paths_all) {
+ return $packages_all;
+ }
+
+ $must_match_ungenerated = array();
+ foreach ($packages_all as $package) {
+ if ($package->getMustMatchUngeneratedPaths()) {
+ $must_match_ungenerated[] = $package;
+ }
+ }
+
+ // If no affected packages have the "Ignore Generated Paths" flag set, we
+ // can't possibly need to throw any away.
+ if (!$must_match_ungenerated) {
+ return $packages_all;
+ }
+
+ if ($paths_ungenerated) {
+ $packages_ungenerated = self::loadAffectedPackages(
+ $repository,
+ $paths_ungenerated);
+ } else {
+ $packages_ungenerated = array();
+ }
+
+ // We have some generated paths, and some packages that ignore generated
+ // paths. Take all the packages which:
+ //
+ // - ignore generated paths; and
+ // - didn't match any ungenerated paths
+ //
+ // ...and remove them from the list.
+
+ $must_match_ungenerated = mpull($must_match_ungenerated, null, 'getID');
+ $packages_ungenerated = mpull($packages_ungenerated, null, 'getID');
+ $packages_all = mpull($packages_all, null, 'getID');
+
+ foreach ($must_match_ungenerated as $package_id => $package) {
+ if (!isset($packages_ungenerated[$package_id])) {
+ unset($packages_all[$package_id]);
+ }
+ }
+
+ return $packages_all;
+ }
+
public static function loadOwningPackages($repository, $path) {
if (empty($path)) {
return array();
}
return self::loadPackagesForPaths($repository, array($path), 1);
}
private static function loadPackagesForPaths(
PhabricatorRepository $repository,
array $paths,
$limit = 0) {
$fragments = array();
foreach ($paths as $path) {
foreach (self::splitPath($path) as $fragment) {
$fragments[$fragment][$path] = true;
}
}
$package = new PhabricatorOwnersPackage();
$path = new PhabricatorOwnersPath();
$conn = $package->establishConnection('r');
$repository_clause = qsprintf(
$conn,
'AND p.repositoryPHID = %s',
$repository->getPHID());
// NOTE: The list of $paths may be very large if we're coming from
// the OwnersWorker and processing, e.g., an SVN commit which created a new
// branch. Break it apart so that it will fit within 'max_allowed_packet',
// and then merge results in PHP.
$rows = array();
foreach (array_chunk(array_keys($fragments), 1024) as $chunk) {
$indexes = array();
foreach ($chunk as $fragment) {
$indexes[] = PhabricatorHash::digestForIndex($fragment);
}
$rows[] = queryfx_all(
$conn,
'SELECT pkg.id, pkg.dominion, p.excluded, p.path
FROM %T pkg JOIN %T p ON p.packageID = pkg.id
WHERE p.pathIndex IN (%Ls) AND pkg.status IN (%Ls) %Q',
$package->getTableName(),
$path->getTableName(),
$indexes,
array(
self::STATUS_ACTIVE,
),
$repository_clause);
}
$rows = array_mergev($rows);
$ids = self::findLongestPathsPerPackage($rows, $fragments);
if (!$ids) {
return array();
}
arsort($ids);
if ($limit) {
$ids = array_slice($ids, 0, $limit, $preserve_keys = true);
}
$ids = array_keys($ids);
$packages = $package->loadAllWhere('id in (%Ld)', $ids);
$packages = array_select_keys($packages, $ids);
return $packages;
}
public static function loadPackagesForRepository($repository) {
$package = new PhabricatorOwnersPackage();
$ids = ipull(
queryfx_all(
$package->establishConnection('r'),
'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorOwnersPath())->getTableName(),
$repository->getPHID()),
'packageID');
return $package->loadAllWhere('id in (%Ld)', $ids);
}
public static function findLongestPathsPerPackage(array $rows, array $paths) {
// Build a map from each path to all the package paths which match it.
$path_hits = array();
$weak = array();
foreach ($rows as $row) {
$id = $row['id'];
$path = $row['path'];
$length = strlen($path);
$excluded = $row['excluded'];
if ($row['dominion'] === self::DOMINION_WEAK) {
$weak[$id] = true;
}
$matches = $paths[$path];
foreach ($matches as $match => $ignored) {
$path_hits[$match][] = array(
'id' => $id,
'excluded' => $excluded,
'length' => $length,
);
}
}
// For each path, process the matching package paths to figure out which
// packages actually own it.
$path_packages = array();
foreach ($path_hits as $match => $hits) {
$hits = isort($hits, 'length');
$packages = array();
foreach ($hits as $hit) {
$package_id = $hit['id'];
if ($hit['excluded']) {
unset($packages[$package_id]);
} else {
$packages[$package_id] = $hit;
}
}
$path_packages[$match] = $packages;
}
// Remove packages with weak dominion rules that should cede control to
// a more specific package.
if ($weak) {
foreach ($path_packages as $match => $packages) {
// Group packages by length.
$length_map = array();
foreach ($packages as $package_id => $package) {
$length_map[$package['length']][$package_id] = $package;
}
// For each path length, remove all weak packages if there are any
// strong packages of the same length. This makes sure that if there
// are one or more strong claims on a particular path, only those
// claims stand.
foreach ($length_map as $package_list) {
$any_strong = false;
foreach ($package_list as $package_id => $package) {
if (!isset($weak[$package_id])) {
$any_strong = true;
break;
}
}
if ($any_strong) {
foreach ($package_list as $package_id => $package) {
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
}
}
$packages = isort($packages, 'length');
$packages = array_reverse($packages, true);
$best_length = null;
foreach ($packages as $package_id => $package) {
// If this is the first package we've encountered, note its length.
// We're iterating over the packages from longest to shortest match,
// so packages of this length always have the best claim on the path.
if ($best_length === null) {
$best_length = $package['length'];
}
// If this package has the same length as the best length, its claim
// stands.
if ($package['length'] === $best_length) {
continue;
}
// If this is a weak package and does not have the best length,
// cede its claim to the stronger package.
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
$path_packages[$match] = $packages;
}
}
// For each package that owns at least one path, identify the longest
// path it owns.
$package_lengths = array();
foreach ($path_packages as $match => $hits) {
foreach ($hits as $hit) {
$length = $hit['length'];
$id = $hit['id'];
if (empty($package_lengths[$id])) {
$package_lengths[$id] = $length;
} else {
$package_lengths[$id] = max($package_lengths[$id], $length);
}
}
}
return $package_lengths;
}
public static function splitPath($path) {
$result = array(
'/',
);
$parts = explode('/', $path);
$buffer = '/';
foreach ($parts as $part) {
if (!strlen($part)) {
continue;
}
$buffer = $buffer.$part.'/';
$result[] = $buffer;
}
return $result;
}
public function attachPaths(array $paths) {
assert_instances_of($paths, 'PhabricatorOwnersPath');
$this->paths = $paths;
// Drop this cache if we're attaching new paths.
$this->pathRepositoryMap = array();
return $this;
}
public function getPaths() {
return $this->assertAttached($this->paths);
}
public function getPathsForRepository($repository_phid) {
if (isset($this->pathRepositoryMap[$repository_phid])) {
return $this->pathRepositoryMap[$repository_phid];
}
$map = array();
foreach ($this->getPaths() as $path) {
if ($path->getRepositoryPHID() == $repository_phid) {
$map[] = $path;
}
}
$this->pathRepositoryMap[$repository_phid] = $map;
return $this->pathRepositoryMap[$repository_phid];
}
public function attachOwners(array $owners) {
assert_instances_of($owners, 'PhabricatorOwnersOwner');
$this->owners = $owners;
return $this;
}
public function getOwners() {
return $this->assertAttached($this->owners);
}
public function getOwnerPHIDs() {
return mpull($this->getOwners(), 'getUserPHID');
}
public function isOwnerPHID($phid) {
if (!$phid) {
return false;
}
$owner_phids = $this->getOwnerPHIDs();
$owner_phids = array_fuse($owner_phids);
return isset($owner_phids[$phid]);
}
public function getMonogram() {
return 'O'.$this->getID();
}
public function getURI() {
// TODO: Move these to "/O123" for consistency.
return '/owners/package/'.$this->getID().'/';
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isOwnerPHID($viewer->getPHID())) {
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('Owners of a package may always view it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorOwnersPackageTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorOwnersPackageTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('owners.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorOwnersCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE packageID = %d',
id(new PhabricatorOwnersPath())->getTableName(),
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE packageID = %d',
id(new PhabricatorOwnersOwner())->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('The package description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or archived status of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('owners')
->setType('list<map<string, wild>>')
->setDescription(pht('List of package owners.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('review')
->setType('map<string, wild>')
->setDescription(pht('Auto review information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('audit')
->setType('map<string, wild>')
->setDescription(pht('Auto audit information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dominion')
->setType('map<string, wild>')
->setDescription(pht('Dominion setting information.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('ignored')
+ ->setType('map<string, wild>')
+ ->setDescription(pht('Ignored attribute information.')),
);
}
public function getFieldValuesForConduit() {
$owner_list = array();
foreach ($this->getOwners() as $owner) {
$owner_list[] = array(
'ownerPHID' => $owner->getUserPHID(),
);
}
$review_map = self::getAutoreviewOptionsMap();
$review_value = $this->getAutoReview();
if (isset($review_map[$review_value])) {
$review_label = $review_map[$review_value]['name'];
} else {
$review_label = pht('Unknown ("%s")', $review_value);
}
$review = array(
'value' => $review_value,
'label' => $review_label,
);
if ($this->getAuditingEnabled()) {
$audit_value = 'audit';
$audit_label = pht('Auditing Enabled');
} else {
$audit_value = 'none';
$audit_label = pht('No Auditing');
}
$audit = array(
'value' => $audit_value,
'label' => $audit_label,
);
$dominion_value = $this->getDominion();
$dominion_map = self::getDominionOptionsMap();
if (isset($dominion_map[$dominion_value])) {
$dominion_label = $dominion_map[$dominion_value]['name'];
$dominion_short = $dominion_map[$dominion_value]['short'];
} else {
$dominion_label = pht('Unknown ("%s")', $dominion_value);
$dominion_short = pht('Unknown ("%s")', $dominion_value);
}
$dominion = array(
'value' => $dominion_value,
'label' => $dominion_label,
'short' => $dominion_short,
);
+ // Force this to always emit as a JSON object even if empty, never as
+ // a JSON list.
+ $ignored = $this->getIgnoredPathAttributes();
+ if (!$ignored) {
+ $ignored = (object)array();
+ }
+
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'status' => $this->getStatus(),
'owners' => $owner_list,
'review' => $review,
'audit' => $audit,
'dominion' => $dominion,
+ 'ignored' => $ignored,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorOwnersPathsSearchEngineAttachment())
->setAttachmentKey('paths'),
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorOwnersPackageFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorOwnersPackageFerretEngine();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new PhabricatorOwnersPackageNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php
new file mode 100644
index 000000000..b3ad6a58f
--- /dev/null
+++ b/src/applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php
@@ -0,0 +1,85 @@
+<?php
+
+final class PhabricatorOwnersPackageIgnoredTransaction
+ extends PhabricatorOwnersPackageTransactionType {
+
+ const TRANSACTIONTYPE = 'owners.ignored';
+
+ public function generateOldValue($object) {
+ return $object->getIgnoredPathAttributes();
+ }
+
+ public function generateNewValue($object, $value) {
+ return array_fill_keys($value, true);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setIgnoredPathAttributes($value);
+ }
+
+ public function getTitle() {
+ $old = array_keys($this->getOldValue());
+ $new = array_keys($this->getNewValue());
+
+ $add = array_diff($new, $old);
+ $rem = array_diff($old, $new);
+
+ $all_n = new PhutilNumber(count($add) + count($rem));
+ $add_n = phutil_count($add);
+ $rem_n = phutil_count($rem);
+
+ if ($new && $old) {
+ return pht(
+ '%s changed %s ignored attribute(s), added %s: %s; removed %s: %s.',
+ $this->renderAuthor(),
+ $all_n,
+ $add_n,
+ $this->renderValueList($add),
+ $rem_n,
+ $this->renderValueList($rem));
+ } else if ($new) {
+ return pht(
+ '%s changed %s ignored attribute(s), added %s: %s.',
+ $this->renderAuthor(),
+ $all_n,
+ $add_n,
+ $this->rendervalueList($add));
+ } else {
+ return pht(
+ '%s changed %s ignored attribute(s), removed %s: %s.',
+ $this->renderAuthor(),
+ $all_n,
+ $rem_n,
+ $this->rendervalueList($rem));
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $valid_attributes = array(
+ 'generated' => true,
+ );
+
+ foreach ($xactions as $xaction) {
+ $new = $xaction->getNewValue();
+
+ foreach ($new as $attribute) {
+ if (isset($valid_attributes[$attribute])) {
+ continue;
+ }
+
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Changeset attribute "%s" is not valid. Valid changeset '.
+ 'attributes are: %s.',
+ $attribute,
+ implode(', ', array_keys($valid_attributes))),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php
index 984b26b8a..753b6ff9e 100644
--- a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php
+++ b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php
@@ -1,221 +1,225 @@
<?php
final class PhabricatorOwnersPackagePathsTransaction
extends PhabricatorOwnersPackageTransactionType {
const TRANSACTIONTYPE = 'owners.paths';
public function generateOldValue($object) {
$paths = $object->getPaths();
return mpull($paths, 'getRef');
}
public function generateNewValue($object, $value) {
$new = $value;
foreach ($new as $key => $info) {
$new[$key]['excluded'] = (int)idx($info, 'excluded');
}
return $new;
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if (!$xactions) {
return $errors;
}
$old = mpull($object->getPaths(), 'getRef');
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
// Check that we have a list of paths.
if (!is_array($new)) {
$errors[] = $this->newInvalidError(
pht('Path specification must be a list of paths.'),
$xaction);
continue;
}
// Check that each item in the list is formatted properly.
$type_exception = null;
foreach ($new as $key => $value) {
try {
PhutilTypeSpec::checkMap(
$value,
array(
'repositoryPHID' => 'string',
'path' => 'string',
'excluded' => 'optional wild',
));
} catch (PhutilTypeCheckException $ex) {
$errors[] = $this->newInvalidError(
pht(
'Path specification list contains invalid value '.
'in key "%s": %s.',
$key,
$ex->getMessage()),
$xaction);
$type_exception = $ex;
}
}
if ($type_exception) {
continue;
}
// Check that any new paths reference legitimate repositories which
// the viewer has permission to see.
list($rem, $add) = PhabricatorOwnersPath::getTransactionValueChanges(
$old,
$new);
if ($add) {
$repository_phids = ipull($add, 'repositoryPHID');
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getActor())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($add as $ref) {
$repository_phid = $ref['repositoryPHID'];
if (isset($repositories[$repository_phid])) {
continue;
}
$errors[] = $this->newInvalidError(
pht(
'Path specification list references repository PHID "%s", '.
'but that is not a valid, visible repository.',
$repository_phid));
}
}
}
return $errors;
}
public function applyExternalEffects($object, $value) {
$old = $this->generateOldValue($object);
$new = $value;
$paths = $object->getPaths();
// We store paths in a normalized format with a trailing slash, regardless
// of whether the user enters "path/to/file.c" or "src/backend/". Normalize
// paths now.
$display_map = array();
+ $seen_map = array();
foreach ($new as $key => $spec) {
$display_path = $spec['path'];
$raw_path = rtrim($display_path, '/').'/';
- // If the user entered two paths which normalize to the same value
- // (like "src/main.c" and "src/main.c/"), discard the duplicates.
- if (isset($display_map[$raw_path])) {
+ // If the user entered two paths in the same repository which normalize
+ // to the same value (like "src/main.c" and "src/main.c/"), discard the
+ // duplicates.
+ $repository_phid = $spec['repositoryPHID'];
+ if (isset($seen_map[$repository_phid][$raw_path])) {
unset($new[$key]);
continue;
}
$new[$key]['path'] = $raw_path;
$display_map[$raw_path] = $display_path;
+ $seen_map[$repository_phid][$raw_path] = true;
}
$diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new);
list($rem, $add) = $diffs;
$set = PhabricatorOwnersPath::getSetFromTransactionValue($rem);
foreach ($paths as $path) {
$ref = $path->getRef();
if (PhabricatorOwnersPath::isRefInSet($ref, $set)) {
$path->delete();
continue;
}
// If the user has changed the display value for a path but the raw
// storage value hasn't changed, update the display value.
if (isset($display_map[$path->getPath()])) {
$path
->setPathDisplay($display_map[$path->getPath()])
->save();
continue;
}
}
foreach ($add as $ref) {
$path = PhabricatorOwnersPath::newFromRef($ref)
->setPackageID($object->getID())
->setPathDisplay($display_map[$ref['path']])
->save();
}
}
public function getTitle() {
// TODO: Flesh this out.
return pht(
'%s updated paths for this package.',
$this->renderAuthor());
}
public function hasChangeDetailView() {
return true;
}
public function newChangeDetailView() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new);
list($rem, $add) = $diffs;
$rows = array();
foreach ($rem as $ref) {
$rows[] = array(
'class' => 'diff-removed',
'change' => '-',
) + $ref;
}
foreach ($add as $ref) {
$rows[] = array(
'class' => 'diff-added',
'change' => '+',
) + $ref;
}
$rowc = array();
foreach ($rows as $key => $row) {
$rowc[] = $row['class'];
$rows[$key] = array(
$row['change'],
$row['excluded'] ? pht('Exclude') : pht('Include'),
$this->renderHandle($row['repositoryPHID']),
$row['path'],
);
}
$table = id(new AphrontTableView($rows))
->setViewer($this->getViewer())
->setRowClasses($rowc)
->setHeaders(
array(
null,
pht('Type'),
pht('Repository'),
pht('Path'),
))
->setColumnClasses(
array(
null,
null,
null,
'wide',
));
return $table;
}
}
diff --git a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
index 3badd04da..c62ecd049 100644
--- a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
+++ b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
@@ -1,82 +1,85 @@
<?php
final class PasteCreateConduitAPIMethod extends PasteConduitAPIMethod {
public function getAPIMethodName() {
return 'paste.create';
}
public function getMethodDescription() {
return pht('Create a new paste.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_FROZEN;
}
public function getMethodStatusDescription() {
return pht(
'This method is frozen and will eventually be deprecated. New code '.
'should use "paste.edit" instead.');
}
protected function defineParamTypes() {
return array(
'content' => 'required string',
'title' => 'optional string',
'language' => 'optional string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
'ERR-NO-PASTE' => pht('Paste may not be empty.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$content = $request->getValue('content');
$title = $request->getValue('title');
$language = $request->getValue('language');
if (!strlen($content)) {
throw new ConduitException('ERR-NO-PASTE');
}
$title = nonempty($title, pht('Masterwork From Distant Lands'));
$language = nonempty($language, '');
$viewer = $request->getUser();
$paste = PhabricatorPaste::initializeNewPaste($viewer);
$xactions = array();
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteContentTransaction::TRANSACTIONTYPE)
->setNewValue($content);
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title);
- $xactions[] = id(new PhabricatorPasteTransaction())
- ->setTransactionType(PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE)
- ->setNewValue($language);
+ if (strlen($language)) {
+ $xactions[] = id(new PhabricatorPasteTransaction())
+ ->setTransactionType(
+ PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE)
+ ->setNewValue($language);
+ }
$editor = id(new PhabricatorPasteEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSource($request->newContentSource());
$xactions = $editor->applyTransactions($paste, $xactions);
$paste->attachRawContent($content);
return $this->buildPasteInfoDictionary($paste);
}
}
diff --git a/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php b/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php
index 24ac71860..80dbc10ee 100644
--- a/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php
+++ b/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php
@@ -1,101 +1,109 @@
<?php
final class PhabricatorPasteTestDataGenerator
extends PhabricatorTestDataGenerator {
const GENERATORKEY = 'pastes';
public function getGeneratorName() {
return pht('Pastes');
}
public function generateObject() {
$author = $this->loadRandomUser();
list($name, $language, $content) = $this->newPasteContent();
$paste = PhabricatorPaste::initializeNewPaste($author);
$xactions = array();
$xactions[] = $this->newTransaction(
PhabricatorPasteTitleTransaction::TRANSACTIONTYPE,
$name);
- $xactions[] = $this->newTransaction(
- PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE,
- $language);
+ if (strlen($language) > 0) {
+ $xactions[] = $this->newTransaction(
+ PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE,
+ $language);
+ }
$xactions[] = $this->newTransaction(
PhabricatorPasteContentTransaction::TRANSACTIONTYPE,
$content);
$editor = id(new PhabricatorPasteEditor())
->setActor($author)
->setContentSource($this->getLipsumContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($paste, $xactions);
return $paste;
}
protected function newEmptyTransaction() {
return new PhabricatorPasteTransaction();
}
- private function newPasteContent() {
- $languages = array(
- 'txt' => array(),
- 'php' => array(
- 'content' => 'PhutilPHPCodeSnippetContextFreeGrammar',
- ),
- 'java' => array(
- 'content' => 'PhutilJavaCodeSnippetContextFreeGrammar',
- ),
- );
+ public function getSupportedLanguages() {
+ return array(
+ 'php' => array(
+ 'content' => 'PhutilPHPCodeSnippetContextFreeGrammar',
+ ),
+ 'java' => array(
+ 'content' => 'PhutilJavaCodeSnippetContextFreeGrammar',
+ ),
+ );
+ }
+
+ public function generateContent($spec) {
+ $content_generator = idx($spec, 'content');
+ if (!$content_generator) {
+ $content_generator = 'PhutilLipsumContextFreeGrammar';
+ }
+ return newv($content_generator, array())
+ ->generateSeveral($this->roll(4, 12, 10));
+ }
+
+ private function newPasteContent() {
+ $languages = $this->getSupportedLanguages();
$language = array_rand($languages);
$spec = $languages[$language];
$title_generator = idx($spec, 'title');
if (!$title_generator) {
$title_generator = 'PhabricatorPasteFilenameContextFreeGrammar';
}
- $content_generator = idx($spec, 'content');
- if (!$content_generator) {
- $content_generator = 'PhutilLipsumContextFreeGrammar';
- }
-
$title = newv($title_generator, array())
->generate();
- $content = newv($content_generator, array())
- ->generateSeveral($this->roll(4, 12, 10));
+ $content = $this->generateContent($spec);
// Usually add the language as a suffix.
if ($this->roll(1, 20) > 2) {
$title = $title.'.'.$language;
}
switch ($this->roll(1, 20)) {
case 1:
// On critical miss, set a different, random language.
$highlight_as = array_rand($languages);
break;
case 18:
case 19:
case 20:
// Sometimes set it to the correct language.
$highlight_as = $language;
break;
default:
// Usually leave it as autodetect.
$highlight_as = '';
break;
}
return array($title, $highlight_as, $content);
}
}
diff --git a/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php b/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php
index a0c060d43..8927270c0 100644
--- a/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php
+++ b/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php
@@ -1,41 +1,57 @@
<?php
final class PhabricatorPasteLanguageTransaction
extends PhabricatorPasteTransactionType {
const TRANSACTIONTYPE = 'paste.language';
public function generateOldValue($object) {
return $object->getLanguage();
}
public function applyInternalEffects($object, $value) {
$object->setLanguage($value);
}
private function renderLanguageValue($value) {
if (!strlen($value)) {
return $this->renderValue(pht('autodetect'));
} else {
return $this->renderValue($value);
}
}
public function getTitle() {
return pht(
"%s updated the paste's language from %s to %s.",
$this->renderAuthor(),
$this->renderLanguageValue($this->getOldValue()),
$this->renderLanguageValue($this->getNewValue()));
}
public function getTitleForFeed() {
return pht(
'%s updated the language for %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderLanguageValue($this->getOldValue()),
$this->renderLanguageValue($this->getNewValue()));
}
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $new = $xaction->getNewValue();
+
+ if ($new !== null && !strlen($new)) {
+ $errors[] = $this->newInvalidError(
+ pht('Paste language must be null or a nonempty string.'),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
}
diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php
index 6322b29b2..9238d8da3 100644
--- a/src/applications/people/application/PhabricatorPeopleApplication.php
+++ b/src/applications/people/application/PhabricatorPeopleApplication.php
@@ -1,110 +1,113 @@
<?php
final class PhabricatorPeopleApplication extends PhabricatorApplication {
public function getName() {
return pht('People');
}
public function getShortDescription() {
return pht('User Accounts and Profiles');
}
public function getBaseURI() {
return '/people/';
}
public function getTitleGlyph() {
return "\xE2\x99\x9F";
}
public function getIcon() {
return 'fa-users';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return $viewer->getIsAdmin();
}
public function getFlavorText() {
return pht('Sort of a social utility.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function canUninstall() {
return false;
}
public function getRoutes() {
return array(
'/people/' => array(
$this->getQueryRoutePattern() => 'PhabricatorPeopleListController',
'logs/' => array(
$this->getQueryRoutePattern() => 'PhabricatorPeopleLogsController',
),
'invite/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorPeopleInviteListController',
'send/'
=> 'PhabricatorPeopleInviteSendController',
),
'approve/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleApproveController',
'(?P<via>disapprove)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorPeopleDisableController',
'(?P<via>disable)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorPeopleDisableController',
'empower/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleEmpowerController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleDeleteController',
'rename/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleRenameController',
'welcome/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleWelcomeController',
'create/' => 'PhabricatorPeopleCreateController',
'new/(?P<type>[^/]+)/' => 'PhabricatorPeopleNewController',
'ldap/' => 'PhabricatorPeopleLdapController',
'editprofile/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileEditController',
'badges/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileBadgesController',
'tasks/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileTasksController',
'commits/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileCommitsController',
'revisions/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileRevisionsController',
'picture/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfilePictureController',
'manage/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileManageController',
),
'/p/(?P<username>[\w._-]+)/' => array(
'' => 'PhabricatorPeopleProfileViewController',
'item/' => $this->getProfileMenuRouting(
'PhabricatorPeopleProfileMenuController'),
),
);
}
public function getRemarkupRules() {
return array(
new PhabricatorMentionRemarkupRule(),
);
}
protected function getCustomCapabilities() {
return array(
PeopleCreateUsersCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
+ PeopleDisableUsersCapability::CAPABILITY => array(
+ 'default' => PhabricatorPolicies::POLICY_ADMIN,
+ ),
PeopleBrowseUserDirectoryCapability::CAPABILITY => array(),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorPeopleUserPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/people/capability/PeopleDisableUsersCapability.php b/src/applications/people/capability/PeopleDisableUsersCapability.php
new file mode 100644
index 000000000..bb58ed2e7
--- /dev/null
+++ b/src/applications/people/capability/PeopleDisableUsersCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PeopleDisableUsersCapability
+ extends PhabricatorPolicyCapability {
+
+ const CAPABILITY = 'people.disable.users';
+
+ public function getCapabilityName() {
+ return pht('Can Disable Users');
+ }
+
+ public function describeCapabilityRejection() {
+ return pht('You do not have permission to disable or enable users.');
+ }
+
+}
diff --git a/src/applications/people/conduit/UserDisableConduitAPIMethod.php b/src/applications/people/conduit/UserDisableConduitAPIMethod.php
index 3512d3f74..a6f2e2c4f 100644
--- a/src/applications/people/conduit/UserDisableConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserDisableConduitAPIMethod.php
@@ -1,53 +1,73 @@
<?php
final class UserDisableConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.disable';
}
public function getMethodDescription() {
return pht('Permanently disable specified users (admin only).');
}
+ public function getMethodStatus() {
+ return self::METHOD_STATUS_DEPRECATED;
+ }
+
+ public function getMethodStatusDescription() {
+ return pht('Obsoleted by method "user.edit".');
+ }
+
protected function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
'ERR-PERMISSIONS' => pht('Only admins can call this method.'),
'ERR-BAD-PHID' => pht('Non existent user PHID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$actor = $request->getUser();
if (!$actor->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
$phids = $request->getValue('phids');
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)',
$phids);
if (count($phids) != count($users)) {
throw new ConduitException('ERR-BAD-PHID');
}
- foreach ($users as $user) {
- id(new PhabricatorUserEditor())
- ->setActor($actor)
- ->disableUser($user, true);
+ foreach ($phids as $phid) {
+ $params = array(
+ 'transactions' => array(
+ array(
+ 'type' => 'disabled',
+ 'value' => true,
+ ),
+ ),
+ 'objectIdentifier' => $phid,
+ );
+
+ id(new ConduitCall('user.edit', $params))
+ ->setUser($actor)
+ ->execute();
}
+
+ return null;
}
}
diff --git a/src/applications/people/conduit/UserEditConduitAPIMethod.php b/src/applications/people/conduit/UserEditConduitAPIMethod.php
new file mode 100644
index 000000000..edc4204c3
--- /dev/null
+++ b/src/applications/people/conduit/UserEditConduitAPIMethod.php
@@ -0,0 +1,20 @@
+<?php
+
+final class UserEditConduitAPIMethod
+ extends PhabricatorEditEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'user.edit';
+ }
+
+ public function newEditEngine() {
+ return new PhabricatorUserEditEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht(
+ 'Apply transactions to edit a user. (Users can not be created via '.
+ 'the API.)');
+ }
+
+}
diff --git a/src/applications/people/conduit/UserEnableConduitAPIMethod.php b/src/applications/people/conduit/UserEnableConduitAPIMethod.php
index f7cb117df..3e4e09b77 100644
--- a/src/applications/people/conduit/UserEnableConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserEnableConduitAPIMethod.php
@@ -1,53 +1,73 @@
<?php
final class UserEnableConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.enable';
}
public function getMethodDescription() {
return pht('Re-enable specified users (admin only).');
}
+ public function getMethodStatus() {
+ return self::METHOD_STATUS_DEPRECATED;
+ }
+
+ public function getMethodStatusDescription() {
+ return pht('Obsoleted by method "user.edit".');
+ }
+
protected function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
'ERR-PERMISSIONS' => pht('Only admins can call this method.'),
'ERR-BAD-PHID' => pht('Non existent user PHID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$actor = $request->getUser();
if (!$actor->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
$phids = $request->getValue('phids');
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)',
$phids);
if (count($phids) != count($users)) {
throw new ConduitException('ERR-BAD-PHID');
}
- foreach ($users as $user) {
- id(new PhabricatorUserEditor())
- ->setActor($actor)
- ->disableUser($user, false);
+ foreach ($phids as $phid) {
+ $params = array(
+ 'transactions' => array(
+ array(
+ 'type' => 'disabled',
+ 'value' => false,
+ ),
+ ),
+ 'objectIdentifier' => $phid,
+ );
+
+ id(new ConduitCall('user.edit', $params))
+ ->setUser($actor)
+ ->execute();
}
+
+ return null;
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleApproveController.php b/src/applications/people/controller/PhabricatorPeopleApproveController.php
index 58cd2e211..0e97ad6ee 100644
--- a/src/applications/people/controller/PhabricatorPeopleApproveController.php
+++ b/src/applications/people/controller/PhabricatorPeopleApproveController.php
@@ -1,57 +1,64 @@
<?php
final class PhabricatorPeopleApproveController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$done_uri = $this->getApplicationURI('query/approval/');
+ if ($user->getIsApproved()) {
+ return $this->newDialog()
+ ->setTitle(pht('Already Approved'))
+ ->appendChild(pht('This user has already been approved.'))
+ ->addCancelButton($done_uri);
+ }
+
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($viewer)
->approveUser($user, true);
$title = pht(
'Phabricator Account "%s" Approved',
$user->getUsername());
$body = sprintf(
"%s\n\n %s\n\n",
pht(
'Your Phabricator account (%s) has been approved by %s. You can '.
'login here:',
$user->getUsername(),
$viewer->getUsername()),
PhabricatorEnv::getProductionURI('/'));
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($user->getPHID()))
->addCCs(array($viewer->getPHID()))
->setSubject('[Phabricator] '.$title)
->setForceDelivery(true)
->setBody($body)
->saveAndSend();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
return $this->newDialog()
->setTitle(pht('Confirm Approval'))
->appendChild(
pht(
'Allow %s to access this Phabricator install?',
phutil_tag('strong', array(), $user->getUsername())))
->addCancelButton($done_uri)
->addSubmitButton(pht('Approve Account'));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleDisableController.php b/src/applications/people/controller/PhabricatorPeopleDisableController.php
index a556f8e51..9f2718086 100644
--- a/src/applications/people/controller/PhabricatorPeopleDisableController.php
+++ b/src/applications/people/controller/PhabricatorPeopleDisableController.php
@@ -1,79 +1,118 @@
<?php
final class PhabricatorPeopleDisableController
extends PhabricatorPeopleController {
+ public function shouldRequireAdmin() {
+ return false;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
- $via = $request->getURIData('id');
+ $via = $request->getURIData('via');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
// NOTE: We reach this controller via the administrative "Disable User"
// on profiles and also via the "X" action on the approval queue. We do
// things slightly differently depending on the context the actor is in.
+ // In particular, disabling via "Disapprove" requires you be an
+ // administrator (and bypasses the "Can Disable Users" permission).
+ // Disabling via "Disable" requires the permission only.
+
$is_disapprove = ($via == 'disapprove');
if ($is_disapprove) {
$done_uri = $this->getApplicationURI('query/approval/');
+
+ if (!$viewer->getIsAdmin()) {
+ return $this->newDialog()
+ ->setTitle(pht('No Permission'))
+ ->appendParagraph(pht('Only administrators can disapprove users.'))
+ ->addCancelButton($done_uri);
+ }
+
+ if ($user->getIsApproved()) {
+ return $this->newDialog()
+ ->setTitle(pht('Already Approved'))
+ ->appendParagraph(pht('This user has already been approved.'))
+ ->addCancelButton($done_uri);
+ }
+
+ // On the "Disapprove" flow, bypass the "Can Disable Users" permission.
+ $actor = PhabricatorUser::getOmnipotentUser();
$should_disable = true;
} else {
+ $this->requireApplicationCapability(
+ PeopleDisableUsersCapability::CAPABILITY);
+
+ $actor = $viewer;
$done_uri = $this->getApplicationURI("manage/{$id}/");
$should_disable = !$user->getIsDisabled();
}
if ($viewer->getPHID() == $user->getPHID()) {
return $this->newDialog()
->setTitle(pht('Something Stays Your Hand'))
->appendParagraph(
pht(
'Try as you might, you find you can not disable your own account.'))
->addCancelButton($done_uri, pht('Curses!'));
}
if ($request->isFormPost()) {
- id(new PhabricatorUserEditor())
- ->setActor($viewer)
- ->disableUser($user, $should_disable);
+ $xactions = array();
+
+ $xactions[] = id(new PhabricatorUserTransaction())
+ ->setTransactionType(PhabricatorUserDisableTransaction::TRANSACTIONTYPE)
+ ->setNewValue($should_disable);
+
+ id(new PhabricatorUserTransactionEditor())
+ ->setActor($actor)
+ ->setActingAsPHID($viewer->getPHID())
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnMissingFields(true)
+ ->setContinueOnNoEffect(true)
+ ->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
if ($should_disable) {
$title = pht('Disable User?');
$short_title = pht('Disable User');
$body = pht(
'Disable %s? They will no longer be able to access Phabricator or '.
'receive email.',
phutil_tag('strong', array(), $user->getUsername()));
$submit = pht('Disable User');
} else {
$title = pht('Enable User?');
$short_title = pht('Enable User');
$body = pht(
'Enable %s? They will be able to access Phabricator and receive '.
'email again.',
phutil_tag('strong', array(), $user->getUsername()));
$submit = pht('Enable User');
}
return $this->newDialog()
->setTitle($title)
->setShortTitle($short_title)
->appendParagraph($body)
->addCancelButton($done_uri)
->addSubmitButton($submit);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleNewController.php b/src/applications/people/controller/PhabricatorPeopleNewController.php
index 9f52cf567..3521fb840 100644
--- a/src/applications/people/controller/PhabricatorPeopleNewController.php
+++ b/src/applications/people/controller/PhabricatorPeopleNewController.php
@@ -1,230 +1,235 @@
<?php
final class PhabricatorPeopleNewController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$type = $request->getURIData('type');
$admin = $request->getUser();
+ id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
+ $admin,
+ $request,
+ $this->getApplicationURI());
+
$is_bot = false;
$is_list = false;
switch ($type) {
case 'standard':
$this->requireApplicationCapability(
PeopleCreateUsersCapability::CAPABILITY);
break;
case 'bot':
$is_bot = true;
break;
case 'list':
$is_list = true;
break;
default:
return new Aphront404Response();
}
$user = new PhabricatorUser();
$require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
$e_username = true;
$e_realname = $require_real_name ? true : null;
$e_email = true;
$errors = array();
$welcome_checked = true;
$new_email = null;
if ($request->isFormPost()) {
$welcome_checked = $request->getInt('welcome');
$user->setUsername($request->getStr('username'));
$new_email = $request->getStr('email');
if (!strlen($new_email)) {
$errors[] = pht('Email is required.');
$e_email = pht('Required');
} else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getUsername())) {
$errors[] = pht('Username is required.');
$e_username = pht('Required');
} else if (!PhabricatorUser::validateUsername($user->getUsername())) {
$errors[] = PhabricatorUser::describeValidUsername();
$e_username = pht('Invalid');
} else {
$e_username = null;
}
if (!strlen($user->getRealName()) && $require_real_name) {
$errors[] = pht('Real name is required.');
$e_realname = pht('Required');
} else {
$e_realname = null;
}
if (!$errors) {
try {
$email = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
// Automatically approve the user, since an admin is creating them.
$user->setIsApproved(1);
// If the user is a bot or list, approve their email too.
if ($is_bot || $is_list) {
$email->setIsVerified(1);
}
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email);
if ($is_bot) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeSystemAgentUser($user, true);
}
if ($is_list) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeMailingListUser($user, true);
}
if ($welcome_checked && !$is_bot && !$is_list) {
$user->sendWelcomeEmail($admin);
}
$response = id(new AphrontRedirectResponse())
->setURI('/p/'.$user->getUsername().'/');
return $response;
} catch (AphrontDuplicateKeyQueryException $ex) {
$errors[] = pht('Username and email must be unique.');
$same_username = id(new PhabricatorUser())
->loadOneWhere('username = %s', $user->getUsername());
$same_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $new_email);
if ($same_username) {
$e_username = pht('Duplicate');
}
if ($same_email) {
$e_email = pht('Duplicate');
}
}
}
}
$form = id(new AphrontFormView())
->setUser($admin);
if ($is_bot) {
$title = pht('Create New Bot');
$form->appendRemarkupInstructions(
pht('You are creating a new **bot** user account.'));
} else if ($is_list) {
$title = pht('Create New Mailing List');
$form->appendRemarkupInstructions(
pht('You are creating a new **mailing list** user account.'));
} else {
$title = pht('Create New User');
$form->appendRemarkupInstructions(
pht('You are creating a new **standard** user account.'));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username')
->setValue($user->getUsername())
->setError($e_username))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Real Name'))
->setName('realname')
->setValue($user->getRealName())
->setError($e_realname))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($new_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
if (!$is_bot && !$is_list) {
$form->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'welcome',
1,
pht('Send "Welcome to Phabricator" email with login instructions.'),
$welcome_checked));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Create User')));
if ($is_bot) {
$form
->appendChild(id(new AphrontFormDividerControl()))
->appendRemarkupInstructions(
pht(
'**Why do bot accounts need an email address?**'.
"\n\n".
'Although bots do not normally receive email from Phabricator, '.
'they can interact with other systems which require an email '.
'address. Examples include:'.
"\n\n".
" - If the account takes actions which //send// email, we need ".
" an address to use in the //From// header.\n".
" - If the account creates commits, Git and Mercurial require ".
" an email address for authorship.\n".
" - If you send email //to// Phabricator on behalf of the ".
" account, the address can identify the sender.\n".
" - Some internal authentication functions depend on accounts ".
" having an email address.\n".
"\n\n".
"The address will automatically be verified, so you do not need ".
"to be able to receive mail at this address, and can enter some ".
"invalid or nonexistent (but correctly formatted) address like ".
"`bot@yourcompany.com` if you prefer."));
}
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileEditController.php b/src/applications/people/controller/PhabricatorPeopleProfileEditController.php
index 3eda8a968..4876f4495 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileEditController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileEditController.php
@@ -1,107 +1,106 @@
<?php
final class PhabricatorPeopleProfileEditController
extends PhabricatorPeopleProfileController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfileImage(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$done_uri = $this->getApplicationURI("manage/{$id}/");
$field_list = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_EDIT);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($user);
$validation_exception = null;
if ($request->isFormPost()) {
$xactions = $field_list->buildFieldTransactionsFromRequest(
new PhabricatorUserTransaction(),
$request);
- $editor = id(new PhabricatorUserProfileEditor())
+ $editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
- ->setContentSource(
- PhabricatorContentSource::newFromRequest($request))
+ ->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$title = pht('Edit Profile');
$form = id(new AphrontFormView())
->setUser($viewer);
$field_list->appendFieldsToForm($form);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($done_uri)
->setValue(pht('Save Profile')));
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
$note = null;
if ($allow_public) {
$note = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(pht(
'Information on user profiles on this install is publicly '.
'visible.'));
}
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Profile'))
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Profile'));
$crumbs->setBorder(true);
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Profile: %s', $user->getFullName()))
->setHeaderIcon('fa-pencil');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$note,
$form_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
index e96306bb8..6cc33c81e 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
@@ -1,188 +1,199 @@
<?php
final class PhabricatorPeopleProfileManageController
extends PhabricatorPeopleProfileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfile(true)
->needProfileImage(true)
->needAvailability(true)
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$header = $this->buildProfileHeader();
$curtain = $this->buildCurtain($user);
$properties = $this->buildPropertyView($user);
$name = $user->getUsername();
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Manage'));
$crumbs->setBorder(true);
$manage = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setCurtain($curtain)
->addPropertySection(pht('Details'), $properties);
return $this->newPage()
->setTitle(
array(
pht('Manage User'),
$user->getUsername(),
))
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild(
array(
$manage,
));
}
private function buildPropertyView(PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($user);
$field_list = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($user, $viewer, $view);
return $view;
}
private function buildCurtain(PhabricatorUser $user) {
$viewer = $this->getViewer();
+ $is_self = ($user->getPHID() === $viewer->getPHID());
+
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$user,
PhabricatorPolicyCapability::CAN_EDIT);
+ $is_admin = $viewer->getIsAdmin();
+ $can_admin = ($is_admin && !$is_self);
+
+ $has_disable = $this->hasApplicationCapability(
+ PeopleDisableUsersCapability::CAPABILITY);
+ $can_disable = ($has_disable && !$is_self);
+
+ $can_welcome = ($is_admin && $user->canEstablishWebSessions());
+
$curtain = $this->newCurtainView($user);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Profile'))
->setHref($this->getApplicationURI('editprofile/'.$user->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-picture-o')
->setName(pht('Edit Profile Picture'))
->setHref($this->getApplicationURI('picture/'.$user->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-wrench')
->setName(pht('Edit Settings'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref('/settings/user/'.$user->getUsername().'/'));
if ($user->getIsAdmin()) {
$empower_icon = 'fa-arrow-circle-o-down';
$empower_name = pht('Remove Administrator');
} else {
$empower_icon = 'fa-arrow-circle-o-up';
$empower_name = pht('Make Administrator');
}
$is_admin = $viewer->getIsAdmin();
$is_self = ($user->getPHID() === $viewer->getPHID());
$can_admin = ($is_admin && !$is_self);
// c4science custo
// Only show admin actions to admin user
if ($is_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($empower_icon)
->setName($empower_name)
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('empower/'.$user->getID().'/')));
}
// c4science custo
if ($can_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-tag')
->setName(pht('Change Username'))
->setDisabled(!$is_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('rename/'.$user->getID().'/')));
}
// c4science custo
if ($is_admin) {
if ($user->getIsDisabled()) {
$disable_icon = 'fa-check-circle-o';
$disable_name = pht('Enable User');
} else {
$disable_icon = 'fa-ban';
$disable_name = pht('Disable User');
}
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($disable_icon)
->setName($disable_name)
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('disable/'.$user->getID().'/')));
}
// c4science custo
if ($is_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setName(pht('Delete User'))
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('delete/'.$user->getID().'/')));
$can_welcome = ($is_admin && $user->canEstablishWebSessions());
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-envelope')
->setName(pht('Send Welcome Email'))
->setWorkflow(true)
->setDisabled(!$can_welcome)
->setHref($this->getApplicationURI('welcome/'.$user->getID().'/')));
}
return $curtain;
}
}
diff --git a/src/applications/people/customfield/PhabricatorUserBlurbField.php b/src/applications/people/customfield/PhabricatorUserBlurbField.php
index 04fb41991..222061ad0 100644
--- a/src/applications/people/customfield/PhabricatorUserBlurbField.php
+++ b/src/applications/people/customfield/PhabricatorUserBlurbField.php
@@ -1,88 +1,109 @@
<?php
final class PhabricatorUserBlurbField
extends PhabricatorUserCustomField {
private $value;
public function getFieldKey() {
return 'user:blurb';
}
+ public function getModernFieldKey() {
+ return 'blurb';
+ }
+
+ public function getFieldKeyForConduit() {
+ return $this->getModernFieldKey();
+ }
+
public function getFieldName() {
return pht('Blurb');
}
public function getFieldDescription() {
return pht('Short blurb about the user.');
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return true;
}
public function shouldAppearInPropertyView() {
return true;
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
$this->value = $object->loadUserProfile()->getBlurb();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->loadUserProfile()->getBlurb();
}
public function getNewValueForApplicationTransactions() {
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->loadUserProfile()->setBlurb($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
+ public function setValueFromStorage($value) {
+ $this->value = $value;
+ return $this;
+ }
+
public function renderEditControl(array $handles) {
return id(new PhabricatorRemarkupControl())
->setUser($this->getViewer())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName());
}
public function getApplicationTransactionRemarkupBlocks(
PhabricatorApplicationTransaction $xaction) {
return array(
$xaction->getNewValue(),
);
}
public function renderPropertyViewLabel() {
return null;
}
public function renderPropertyViewValue(array $handles) {
$blurb = $this->getObject()->loadUserProfile()->getBlurb();
if (!strlen($blurb)) {
return null;
}
$viewer = $this->getViewer();
$view = new PHUIRemarkupView($viewer, $blurb);
return $view;
}
public function getStyleForPropertyView() {
return 'block';
}
+ public function shouldAppearInConduitTransactions() {
+ return true;
+ }
+
+ protected function newConduitEditParameterType() {
+ return new ConduitStringParameterType();
+ }
+
}
diff --git a/src/applications/people/customfield/PhabricatorUserIconField.php b/src/applications/people/customfield/PhabricatorUserIconField.php
index a3f43cb7b..0b25a9dd8 100644
--- a/src/applications/people/customfield/PhabricatorUserIconField.php
+++ b/src/applications/people/customfield/PhabricatorUserIconField.php
@@ -1,61 +1,82 @@
<?php
final class PhabricatorUserIconField
extends PhabricatorUserCustomField {
private $value;
public function getFieldKey() {
return 'user:icon';
}
+ public function getModernFieldKey() {
+ return 'icon';
+ }
+
+ public function getFieldKeyForConduit() {
+ return $this->getModernFieldKey();
+ }
+
public function getFieldName() {
return pht('Icon');
}
public function getFieldDescription() {
return pht('User icon to accompany their title.');
}
public function canDisableField() {
return false;
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return true;
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
$this->value = $object->loadUserProfile()->getIcon();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->loadUserProfile()->getIcon();
}
public function getNewValueForApplicationTransactions() {
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->loadUserProfile()->setIcon($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
+ public function setValueFromStorage($value) {
+ $this->value = $value;
+ return $this;
+ }
+
public function renderEditControl(array $handles) {
return id(new PHUIFormIconSetControl())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName())
->setIconSet(new PhabricatorPeopleIconSet());
}
+ public function shouldAppearInConduitTransactions() {
+ return true;
+ }
+
+ protected function newConduitEditParameterType() {
+ return new ConduitStringParameterType();
+ }
+
}
diff --git a/src/applications/people/customfield/PhabricatorUserRealNameField.php b/src/applications/people/customfield/PhabricatorUserRealNameField.php
index 90e490fc3..e7e610fb0 100644
--- a/src/applications/people/customfield/PhabricatorUserRealNameField.php
+++ b/src/applications/people/customfield/PhabricatorUserRealNameField.php
@@ -1,68 +1,89 @@
<?php
final class PhabricatorUserRealNameField
extends PhabricatorUserCustomField {
private $value;
public function getFieldKey() {
return 'user:realname';
}
+ public function getModernFieldKey() {
+ return 'realName';
+ }
+
+ public function getFieldKeyForConduit() {
+ return $this->getModernFieldKey();
+ }
+
public function getFieldName() {
return pht('Real Name');
}
public function getFieldDescription() {
return pht('Stores the real name of the user, like "Abraham Lincoln".');
}
public function canDisableField() {
return false;
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return true;
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
$this->value = $object->getRealName();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->getRealName();
}
public function getNewValueForApplicationTransactions() {
if (!$this->isEditable()) {
return $this->getObject()->getRealName();
}
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->setRealName($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
+ public function setValueFromStorage($value) {
+ $this->value = $value;
+ return $this;
+ }
+
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName())
->setDisabled(!$this->isEditable());
}
private function isEditable() {
return PhabricatorEnv::getEnvConfig('account.editable');
}
+ public function shouldAppearInConduitTransactions() {
+ return true;
+ }
+
+ protected function newConduitEditParameterType() {
+ return new ConduitStringParameterType();
+ }
+
}
diff --git a/src/applications/people/customfield/PhabricatorUserTitleField.php b/src/applications/people/customfield/PhabricatorUserTitleField.php
index af6793e76..5ceb774fa 100644
--- a/src/applications/people/customfield/PhabricatorUserTitleField.php
+++ b/src/applications/people/customfield/PhabricatorUserTitleField.php
@@ -1,60 +1,81 @@
<?php
final class PhabricatorUserTitleField
extends PhabricatorUserCustomField {
private $value;
public function getFieldKey() {
return 'user:title';
}
+ public function getModernFieldKey() {
+ return 'title';
+ }
+
+ public function getFieldKeyForConduit() {
+ return $this->getModernFieldKey();
+ }
+
public function getFieldName() {
return pht('Title');
}
public function getFieldDescription() {
return pht('User title, like "CEO" or "Assistant to the Manager".');
}
public function canDisableField() {
return false;
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return true;
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
$this->value = $object->loadUserProfile()->getTitle();
}
public function getOldValueForApplicationTransactions() {
return $this->getObject()->loadUserProfile()->getTitle();
}
public function getNewValueForApplicationTransactions() {
return $this->value;
}
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
$this->getObject()->loadUserProfile()->setTitle($xaction->getNewValue());
}
public function readValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getFieldKey());
}
+ public function setValueFromStorage($value) {
+ $this->value = $value;
+ return $this;
+ }
+
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setValue($this->value)
->setLabel($this->getFieldName());
}
+ public function shouldAppearInConduitTransactions() {
+ return true;
+ }
+
+ protected function newConduitEditParameterType() {
+ return new ConduitStringParameterType();
+ }
+
}
diff --git a/src/applications/people/editor/PhabricatorUserEditEngine.php b/src/applications/people/editor/PhabricatorUserEditEngine.php
new file mode 100644
index 000000000..c547426b1
--- /dev/null
+++ b/src/applications/people/editor/PhabricatorUserEditEngine.php
@@ -0,0 +1,81 @@
+<?php
+
+final class PhabricatorUserEditEngine
+ extends PhabricatorEditEngine {
+
+ const ENGINECONST = 'people.user';
+
+ public function isEngineConfigurable() {
+ return false;
+ }
+
+ public function getEngineName() {
+ return pht('Users');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Configure User Forms');
+ }
+
+ public function getSummaryText() {
+ return pht('Configure creation and editing forms for users.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorPeopleApplication';
+ }
+
+ protected function newEditableObject() {
+ return new PhabricatorUser();
+ }
+
+ protected function newObjectQuery() {
+ return id(new PhabricatorPeopleQuery());
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create New User');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit User: %s', $object->getUsername());
+ }
+
+ protected function getObjectEditShortText($object) {
+ return $object->getMonogram();
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('Create User');
+ }
+
+ protected function getObjectName() {
+ return pht('User');
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function getCreateNewObjectPolicy() {
+ // At least for now, forbid creating new users via EditEngine. This is
+ // primarily enforcing that "user.edit" can not create users via the API.
+ return PhabricatorPolicies::POLICY_NOONE;
+ }
+
+ protected function buildCustomEditFields($object) {
+ return array(
+ id(new PhabricatorBoolEditField())
+ ->setKey('disabled')
+ ->setOptions(pht('Active'), pht('Disabled'))
+ ->setLabel(pht('Disabled'))
+ ->setDescription(pht('Disable the user.'))
+ ->setTransactionType(PhabricatorUserDisableTransaction::TRANSACTIONTYPE)
+ ->setIsConduitOnly(true)
+ ->setConduitDescription(pht('Disable or enable the user.'))
+ ->setConduitTypeDescription(pht('True to disable the user.'))
+ ->setValue($object->getIsDisabled()),
+ );
+ }
+
+}
diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php
index cda9c1e41..58c2ba1ff 100644
--- a/src/applications/people/editor/PhabricatorUserEditor.php
+++ b/src/applications/people/editor/PhabricatorUserEditor.php
@@ -1,719 +1,686 @@
<?php
/**
* Editor class for creating and adjusting users. This class guarantees data
* integrity and writes logs when user information changes.
*
* @task config Configuration
* @task edit Creating and Editing Users
* @task role Editing Roles
* @task email Adding, Removing and Changing Email
* @task internal Internals
*/
final class PhabricatorUserEditor extends PhabricatorEditor {
private $logs = array();
/* -( Creating and Editing Users )----------------------------------------- */
/**
* @task edit
*/
public function createNewUser(
PhabricatorUser $user,
PhabricatorUserEmail $email,
$allow_reassign = false) {
if ($user->getID()) {
throw new Exception(pht('User has already been created!'));
}
$is_reassign = false;
if ($email->getID()) {
if ($allow_reassign) {
if ($email->getIsPrimary()) {
throw new Exception(
pht('Primary email addresses can not be reassigned.'));
}
$is_reassign = true;
} else {
throw new Exception(pht('Email has already been created!'));
}
}
if (!PhabricatorUser::validateUsername($user->getUsername())) {
$valid = PhabricatorUser::describeValidUsername();
throw new Exception(pht('Username is invalid! %s', $valid));
}
// Always set a new user's email address to primary.
$email->setIsPrimary(1);
// If the primary address is already verified, also set the verified flag
// on the user themselves.
if ($email->getIsVerified()) {
$user->setIsEmailVerified(1);
}
$this->willAddEmail($email);
$user->openTransaction();
try {
$user->save();
$email->setUserPHID($user->getPHID());
$email->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// We might have written the user but failed to write the email; if
// so, erase the IDs we attached.
$user->setID(null);
$user->setPHID(null);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_CREATE);
$log->setNewValue($email->getAddress());
$log->save();
if ($is_reassign) {
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
$log->setNewValue($email->getAddress());
$log->save();
}
$user->saveTransaction();
if ($email->getIsVerified()) {
$this->didVerifyEmail($user, $email);
}
return $this;
}
/**
* @task edit
*/
public function updateUser(
PhabricatorUser $user,
PhabricatorUserEmail $email = null) {
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->save();
if ($email) {
$email->save();
}
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_EDIT);
$log->save();
$user->saveTransaction();
return $this;
}
/**
* @task edit
*/
public function changeUsername(PhabricatorUser $user, $username) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
throw new Exception(pht('Username is invalid! %s', $valid));
}
$old_username = $user->getUsername();
$user->openTransaction();
$user->reload();
$user->setUsername($username);
try {
$user->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$user->setUsername($old_username);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_CHANGE_USERNAME);
$log->setOldValue($old_username);
$log->setNewValue($username);
$log->save();
$user->saveTransaction();
// The SSH key cache currently includes usernames, so dirty it. See T12554
// for discussion.
PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
$user->sendUsernameChangeEmail($actor, $old_username);
}
/* -( Editing Roles )------------------------------------------------------ */
/**
* @task role
*/
public function makeAdminUser(PhabricatorUser $user, $admin) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsAdmin() == $admin) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_ADMIN);
$log->setOldValue($user->getIsAdmin());
$log->setNewValue($admin);
$user->setIsAdmin((int)$admin);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsSystemAgent() == $system_agent) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_SYSTEM_AGENT);
$log->setOldValue($user->getIsSystemAgent());
$log->setNewValue($system_agent);
$user->setIsSystemAgent((int)$system_agent);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function makeMailingListUser(PhabricatorUser $user, $mailing_list) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsMailingList() == $mailing_list) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_MAILING_LIST);
$log->setOldValue($user->getIsMailingList());
$log->setNewValue($mailing_list);
$user->setIsMailingList((int)$mailing_list);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
- /**
- * @task role
- */
- public function disableUser(PhabricatorUser $user, $disable) {
- $actor = $this->requireActor();
-
- if (!$user->getID()) {
- throw new Exception(pht('User has not been created yet!'));
- }
-
- $user->openTransaction();
- $user->beginWriteLocking();
-
- $user->reload();
- if ($user->getIsDisabled() == $disable) {
- $user->endWriteLocking();
- $user->killTransaction();
- return $this;
- }
-
- $log = PhabricatorUserLog::initializeNewLog(
- $actor,
- $user->getPHID(),
- PhabricatorUserLog::ACTION_DISABLE);
- $log->setOldValue($user->getIsDisabled());
- $log->setNewValue($disable);
-
- $user->setIsDisabled((int)$disable);
- $user->save();
-
- $log->save();
-
- $user->endWriteLocking();
- $user->saveTransaction();
-
- return $this;
- }
-
-
/**
* @task role
*/
public function approveUser(PhabricatorUser $user, $approve) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsApproved() == $approve) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_APPROVE);
$log->setOldValue($user->getIsApproved());
$log->setNewValue($approve);
$user->setIsApproved($approve);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/* -( Adding, Removing and Changing Email )-------------------------------- */
/**
* @task email
*/
public function addEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if ($email->getID()) {
throw new Exception(pht('Email has already been created!'));
}
// Use changePrimaryEmail() to change primary email.
$email->setIsPrimary(0);
$email->setUserPHID($user->getPHID());
$this->willAddEmail($email);
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
try {
$email->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$user->endWriteLocking();
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_ADD);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
+ // Try and match this new address against unclaimed `RepositoryIdentity`s
+ PhabricatorWorker::scheduleTask(
+ 'PhabricatorRepositoryIdentityChangeWorker',
+ array('userPHID' => $user->getPHID()),
+ array('objectPHID' => $user->getPHID()));
+
return $this;
}
/**
* @task email
*/
public function removeEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getIsPrimary()) {
throw new Exception(pht("Can't remove primary email!"));
}
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('Email not owned by user!'));
}
$email->delete();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REMOVE);
$log->setOldValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
$this->revokePasswordResetLinks($user);
return $this;
}
/**
* @task email
*/
public function changePrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('User does not own email!'));
}
if ($email->getIsPrimary()) {
throw new Exception(pht('Email is already primary!'));
}
if (!$email->getIsVerified()) {
throw new Exception(pht('Email is not verified!'));
}
$old_primary = $user->loadPrimaryEmail();
if ($old_primary) {
$old_primary->setIsPrimary(0);
$old_primary->save();
}
$email->setIsPrimary(1);
$email->save();
// If the user doesn't have the verified flag set on their account
// yet, set it. We've made sure the email is verified above. See
// T12635 for discussion.
if (!$user->getIsEmailVerified()) {
$user->setIsEmailVerified(1);
$user->save();
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_PRIMARY);
$log->setOldValue($old_primary ? $old_primary->getAddress() : null);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
if ($old_primary) {
$old_primary->sendOldPrimaryEmail($user, $email);
}
$email->sendNewPrimaryEmail($user);
$this->revokePasswordResetLinks($user);
return $this;
}
/**
* Verify a user's email address.
*
* This verifies an individual email address. If the address is the user's
* primary address and their account was not previously verified, their
* account is marked as email verified.
*
* @task email
*/
public function verifyEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('User does not own email!'));
}
if (!$email->getIsVerified()) {
$email->setIsVerified(1);
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_VERIFY);
$log->setNewValue($email->getAddress());
$log->save();
}
if (!$user->getIsEmailVerified()) {
// If the user just verified their primary email address, mark their
// account as email verified.
$user_primary = $user->loadPrimaryEmail();
if ($user_primary->getID() == $email->getID()) {
$user->setIsEmailVerified(1);
$user->save();
}
}
$user->endWriteLocking();
$user->saveTransaction();
$this->didVerifyEmail($user, $email);
}
/**
* Reassign an unverified email address.
*/
public function reassignEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
$old_user = $email->getUserPHID();
if ($old_user != $user->getPHID()) {
if ($email->getIsVerified()) {
throw new Exception(
pht('Verified email addresses can not be reassigned.'));
}
if ($email->getIsPrimary()) {
throw new Exception(
pht('Primary email addresses can not be reassigned.'));
}
$email->setUserPHID($user->getPHID());
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
$log->setNewValue($email->getAddress());
$log->save();
}
$user->endWriteLocking();
$user->saveTransaction();
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function willAddEmail(PhabricatorUserEmail $email) {
// Hard check before write to prevent creation of disallowed email
// addresses. Normally, the application does checks and raises more
// user friendly errors for us, but we omit the courtesy checks on some
// pathways like administrative scripts for simplicity.
if (!PhabricatorUserEmail::isValidAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeValidAddresses());
}
if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeAllowedAddresses());
}
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($email->getAddress()))
->executeOne();
if ($application_email) {
throw new Exception($application_email->getInUseMessage());
}
}
public function revokePasswordResetLinks(PhabricatorUser $user) {
// Revoke any outstanding password reset links. If an attacker compromises
// an account, changes the email address, and sends themselves a password
// reset link, it could otherwise remain live for a short period of time
// and allow them to compromise the account again later.
PhabricatorAuthTemporaryToken::revokeTokens(
$user,
array($user->getPHID()),
array(
PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE,
PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE,
));
}
private function didVerifyEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$event_type = PhabricatorEventType::TYPE_AUTH_DIDVERIFYEMAIL;
$event_data = array(
'user' => $user,
'email' => $email,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/people/editor/PhabricatorUserProfileEditor.php b/src/applications/people/editor/PhabricatorUserTransactionEditor.php
similarity index 73%
rename from src/applications/people/editor/PhabricatorUserProfileEditor.php
rename to src/applications/people/editor/PhabricatorUserTransactionEditor.php
index 9ec02ba2a..c0dfa941a 100644
--- a/src/applications/people/editor/PhabricatorUserProfileEditor.php
+++ b/src/applications/people/editor/PhabricatorUserTransactionEditor.php
@@ -1,15 +1,14 @@
<?php
-final class PhabricatorUserProfileEditor
+final class PhabricatorUserTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getEditorObjectsDescription() {
- return pht('User Profiles');
+ return pht('Users');
}
-
}
diff --git a/src/applications/people/engineextension/PeopleMainMenuBarExtension.php b/src/applications/people/engineextension/PeopleMainMenuBarExtension.php
index 152fb2bec..e19cedd30 100644
--- a/src/applications/people/engineextension/PeopleMainMenuBarExtension.php
+++ b/src/applications/people/engineextension/PeopleMainMenuBarExtension.php
@@ -1,121 +1,122 @@
<?php
final class PeopleMainMenuBarExtension
extends PhabricatorMainMenuBarExtension {
const MAINMENUBARKEY = 'user';
public function isExtensionEnabledForViewer(PhabricatorUser $viewer) {
return $viewer->isLoggedIn();
}
public function shouldRequireFullSession() {
return false;
}
public function getExtensionOrder() {
return 1200;
}
public function buildMainMenus() {
$viewer = $this->getViewer();
$application = $this->getApplication();
$dropdown_menu = $this->newDropdown($viewer, $application);
$menu_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'user-menu',
array(
'menuID' => $menu_id,
'menu' => $dropdown_menu->getDropdownMenuMetadata(),
));
$image = $viewer->getProfileImageURI();
$profile_image = id(new PHUIIconView())
->setImage($image)
->setHeadSize(PHUIIconView::HEAD_SMALL);
$user_menu = id(new PHUIButtonView())
->setID($menu_id)
->setTag('a')
->setHref('/p/'.$viewer->getUsername().'/')
->setIcon($profile_image)
->addClass('phabricator-core-user-menu')
->setHasCaret(true)
- ->setNoCSS(true);
+ ->setNoCSS(true)
+ ->setAuralLabel(pht('Account Menu'));
return array(
$user_menu,
);
}
private function newDropdown(
PhabricatorUser $viewer,
$application) {
$person_to_show = id(new PHUIObjectItemView())
->setObjectName($viewer->getRealName())
->setSubHead($viewer->getUsername())
->setImageURI($viewer->getProfileImageURI());
$user_view = id(new PHUIObjectItemListView())
->setViewer($viewer)
->setFlush(true)
->setSimple(true)
->addItem($person_to_show)
->addClass('phabricator-core-user-profile-object');
$view = id(new PhabricatorActionListView())
->setViewer($viewer);
if ($this->getIsFullSession()) {
$view->addAction(
id(new PhabricatorActionView())
->appendChild($user_view));
$view->addAction(
id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Profile'))
->setHref('/p/'.$viewer->getUsername().'/'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Settings'))
->setHref('/settings/user/'.$viewer->getUsername().'/'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Manage'))
->setHref('/people/manage/'.$viewer->getID().'/'));
if ($application) {
$help_links = $application->getHelpMenuItems($viewer);
if ($help_links) {
foreach ($help_links as $link) {
$view->addAction($link);
}
}
}
$view->addAction(
id(new PhabricatorActionView())
->addSigil('logout-item')
->setType(PhabricatorActionView::TYPE_DIVIDER));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Log Out %s', $viewer->getUsername()))
->addSigil('logout-item')
->setHref('/logout/')
->setWorkflow(true));
return $view;
}
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index 4ce944d90..5fadc2a5c 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1669 +1,1669 @@
<?php
/**
* @task availability Availability
* @task image-cache Profile Image Cache
* @task factors Multi-Factor Authentication
* @task handles Managing Handles
* @task settings Settings
* @task cache User Cache
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorAuthPasswordHashInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
protected $userName;
protected $realName;
protected $profileImagePHID;
protected $defaultProfileImagePHID;
protected $defaultProfileImageVersion;
protected $availabilityCache;
protected $availabilityCacheTTL;
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isMailingList = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
protected $accountSecret;
private $profile = null;
private $availability = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $badgePHIDs = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
private $rawCacheData = array();
private $usableCacheData = array();
private $authorities = array();
private $handlePool;
private $csrfSalt;
private $settingCacheKeys = array();
private $settingCache = array();
private $allowInlineCacheGeneration;
private $conduitClusterToken = self::ATTACHABLE;
protected function readField($field) {
switch ($field) {
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isMailingList':
return (bool)$this->isMailingList;
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
}
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if (!$this->isLoggedIn()) {
return false;
}
if ($this->isOmnipotent()) {
return true;
}
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
/**
* Is this a user who we can reasonably expect to respond to requests?
*
* This is used to provide a grey "disabled/unresponsive" dot cue when
* rendering handles and tags, so it isn't a surprise if you get ignored
* when you ask things of users who will not receive notifications or could
* not respond to them (because they are disabled, unapproved, do not have
* verified email addresses, etc).
*
* @return bool True if this user can receive and respond to requests from
* other humans.
*/
public function isResponsive() {
if (!$this->isUserActivated()) {
return false;
}
if (!$this->getIsEmailVerified()) {
return false;
}
return true;
}
public function canEstablishWebSessions() {
if ($this->getIsMailingList()) {
return false;
}
if ($this->getIsSystemAgent()) {
return false;
}
return true;
}
public function canEstablishAPISessions() {
if ($this->getIsDisabled()) {
return false;
}
// Intracluster requests are permitted even if the user is logged out:
// in particular, public users are allowed to issue intracluster requests
// when browsing Diffusion.
if (PhabricatorEnv::isClusterRemoteAddress()) {
if (!$this->isLoggedIn()) {
return true;
}
}
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
public function canEstablishSSHSessions() {
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'profileImagePHID' => 'phid?',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isMailingList' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
'availabilityCache' => 'text255?',
'availabilityCacheTTL' => 'uint32?',
'defaultProfileImagePHID' => 'phid?',
'defaultProfileImageVersion' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
self::CONFIG_NO_MUTATE => array(
'availabilityCache' => true,
'availabilityCacheTTL' => true,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function saveWithoutIndex() {
return parent::save();
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = $this->saveWithoutIndex();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
return $result;
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
const CSRF_CYCLE_FREQUENCY = 3600;
const CSRF_SALT_LENGTH = 8;
const CSRF_TOKEN_LENGTH = 16;
const CSRF_BREACH_PREFIX = 'B@';
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
private function getRawCSRFToken($offset = 0) {
return $this->generateToken(
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
self::CSRF_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
self::CSRF_TOKEN_LENGTH);
}
public function getCSRFToken() {
// c4science custo
// disable csrf for admin // FIXME: CHECK THIS
if ($this->isOmnipotent() && !$this->getIsAdmin()) {
// We may end up here when called from the daemons. The omnipotent user
// has no meaningful CSRF token, so just return `null`.
return null;
}
if ($this->csrfSalt === null) {
$this->csrfSalt = Filesystem::readRandomCharacters(
self::CSRF_SALT_LENGTH);
}
$salt = $this->csrfSalt;
// Generate a token hash to mitigate BREACH attacks against SSL. See
// discussion in T3684.
$token = $this->getRawCSRFToken();
$hash = PhabricatorHash::weakDigest($token, $salt);
return self::CSRF_BREACH_PREFIX.$salt.substr(
$hash, 0, self::CSRF_TOKEN_LENGTH);
}
public function validateCSRFToken($token) {
// We expect a BREACH-mitigating token. See T3684.
$breach_prefix = self::CSRF_BREACH_PREFIX;
$breach_prelen = strlen($breach_prefix);
if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) {
return false;
}
$salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
$token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
// When the user posts a form, we check that it contains a valid CSRF token.
// Tokens cycle each hour (every CSRF_CYCLE_FREQUENCY seconds) and we accept
// either the current token, the next token (users can submit a "future"
// token if you have two web frontends that have some clock skew) or any of
// the last 6 tokens. This means that pages are valid for up to 7 hours.
// There is also some Javascript which periodically refreshes the CSRF
// tokens on each page, so theoretically pages should be valid indefinitely.
// However, this code may fail to run (if the user loses their internet
// connection, or there's a JS problem, or they don't have JS enabled).
// Choosing the size of the window in which we accept old CSRF tokens is
// an issue of balancing concerns between security and usability. We could
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
// attacks using captured CSRF tokens, but it's also more likely that real
// users will be affected by this, e.g. if they close their laptop for an
// hour, open it back up, and try to submit a form before the CSRF refresh
// can kick in. Since the user experience of submitting a form with expired
// CSRF is often quite bad (you basically lose data, or it's a big pain to
// recover at least) and I believe we gain little additional protection
// by keeping the window very short (the overwhelming value here is in
// preventing blind attacks, and most attacks which can capture CSRF tokens
// can also just capture authentication information [sniffing networks]
// or act as the user [xss]) the 7 hour default seems like a reasonable
// balance. Other major platforms have much longer CSRF token lifetimes,
// like Rails (session duration) and Django (forever), which suggests this
// is a reasonable analysis.
$csrf_window = 6;
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getRawCSRFToken($ii);
$digest = PhabricatorHash::weakDigest($valid, $salt);
$digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH);
if (phutil_hashes_are_identical($digest, $token)) {
return true;
}
}
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
if ($this->getPHID()) {
$vec = $this->getPHID().$this->getAccountSecret();
} else {
$vec = $this->getAlternateCSRFString();
}
if ($this->hasSession()) {
$vec = $vec.$this->getSession()->getSessionKey();
}
$time_block = floor($epoch / $frequency);
$vec = $vec.$key.$time_block;
return substr(PhabricatorHash::weakDigest($vec), 0, $len);
}
public function getUserProfile() {
return $this->assertAttached($this->profile);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$this->profile = PhabricatorUserProfile::initializeNewProfile($this);
}
return $this->profile;
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception(pht('User has no primary email address!'));
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
/* -( Settings )----------------------------------------------------------- */
public function getUserSetting($key) {
// NOTE: We store available keys and cached values separately to make it
// faster to check for `null` in the cache, which is common.
if (isset($this->settingCacheKeys[$key])) {
return $this->settingCache[$key];
}
$settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
if ($this->getPHID()) {
$settings = $this->requireCacheData($settings_key);
} else {
$settings = $this->loadGlobalSettings();
}
if (array_key_exists($key, $settings)) {
$value = $settings[$key];
return $this->writeUserSettingCache($key, $value);
}
$cache = PhabricatorCaches::getRuntimeCache();
$cache_key = "settings.defaults({$key})";
$cache_map = $cache->getKeys(array($cache_key));
if ($cache_map) {
$value = $cache_map[$cache_key];
} else {
$defaults = PhabricatorSetting::getAllSettings();
if (isset($defaults[$key])) {
$value = id(clone $defaults[$key])
->setViewer($this)
->getSettingDefaultValue();
} else {
$value = null;
}
$cache->setKey($cache_key, $value);
}
return $this->writeUserSettingCache($key, $value);
}
/**
* Test if a given setting is set to a particular value.
*
* @param const Setting key.
* @param wild Value to compare.
* @return bool True if the setting has the specified value.
* @task settings
*/
public function compareUserSetting($key, $value) {
$actual = $this->getUserSetting($key);
return ($actual == $value);
}
private function writeUserSettingCache($key, $value) {
$this->settingCacheKeys[$key] = true;
$this->settingCache[$key] = $value;
return $value;
}
public function getTranslation() {
return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);
}
public function getTimezoneIdentifier() {
return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
}
public static function getGlobalSettingsCacheKey() {
return 'user.settings.globals.v1';
}
private function loadGlobalSettings() {
$cache_key = self::getGlobalSettingsCacheKey();
$cache = PhabricatorCaches::getMutableStructureCache();
$settings = $cache->getKey($cache_key);
if (!$settings) {
$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
$settings = $preferences->getPreferences();
$cache->setKey($cache_key, $settings);
}
return $settings;
}
/**
* Override the user's timezone identifier.
*
* This is primarily useful for unit tests.
*
* @param string New timezone identifier.
* @return this
* @task settings
*/
public function overrideTimezoneIdentifier($identifier) {
$timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
$this->settingCacheKeys[$timezone_key] = true;
$this->settingCache[$timezone_key] = $identifier;
return $this;
}
public function getGender() {
return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);
}
public function loadEditorLink(
$path,
$line,
PhabricatorRepository $repository = null) {
$editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
if (is_array($path)) {
$multi_key = PhabricatorEditorMultipleSetting::SETTINGKEY;
$multiedit = $this->getUserSetting($multi_key);
switch ($multiedit) {
case PhabricatorEditorMultipleSetting::VALUE_SPACES:
$path = implode(' ', $path);
break;
case PhabricatorEditorMultipleSetting::VALUE_SINGLE:
default:
return null;
}
}
if (!strlen($editor)) {
return null;
}
if ($repository) {
$callsign = $repository->getCallsign();
} else {
$callsign = null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
public function getAlternateCSRFString() {
return $this->assertAttached($this->alternateCSRFString);
}
public function attachAlternateCSRFString($string) {
$this->alternateCSRFString = $string;
return $this;
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %Q',
$table,
implode(', ', $sql));
}
}
public function sendWelcomeEmail(PhabricatorUser $admin) {
if (!$this->canEstablishWebSessions()) {
throw new Exception(
pht(
'Can not send welcome mail to users who can not establish '.
'web sessions!'));
}
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$user_username = $this->getUserName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$base_uri = PhabricatorEnv::getProductionURI('/');
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
$this->loadPrimaryEmail(),
PhabricatorAuthSessionEngine::ONETIME_WELCOME);
$body = pht(
"Welcome to Phabricator!\n\n".
"%s (%s) has created an account for you.\n\n".
" Username: %s\n\n".
"To login to Phabricator, follow this link and set a password:\n\n".
" %s\n\n".
"After you have set a password, you can login in the future by ".
"going here:\n\n".
" %s\n",
$admin_username,
$admin_realname,
$user_username,
$uri,
$base_uri);
if (!$is_serious) {
$body .= sprintf(
"\n%s\n",
pht("Love,\nPhabricator"));
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Welcome to Phabricator'))
->setBody($body)
->saveAndSend();
}
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
null,
PhabricatorAuthSessionEngine::ONETIME_USERNAME);
$password_instructions = sprintf(
"%s\n\n %s\n\n%s\n",
pht(
"If you use a password to login, you'll need to reset it ".
"before you can login again. You can reset your password by ".
"following this link:"),
$uri,
pht(
"And, of course, you'll need to use your new username to login ".
"from now on. If you use OAuth to login, nothing should change."));
}
$body = sprintf(
"%s\n\n %s\n %s\n\n%s",
pht(
'%s (%s) has changed your Phabricator username.',
$admin_username,
$admin_realname),
pht(
'Old Username: %s',
$old_username),
pht(
'New Username: %s',
$new_username),
$password_instructions);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Username Changed'))
->setBody($body)
->saveAndSend();
}
public static function describeValidUsername() {
return pht(
'Usernames must contain only numbers, letters, period, underscore and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function getProfileImageURI() {
$uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;
return $this->requireCacheData($uri_key);
}
public function getUnreadNotificationCount() {
$notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;
return $this->requireCacheData($notification_key);
}
public function getUnreadMessageCount() {
$message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;
return $this->requireCacheData($message_key);
}
public function getRecentBadgeAwards() {
$badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
return $this->requireCacheData($badges_key);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function getTimeZone() {
return new DateTimeZone($this->getTimezoneIdentifier());
}
public function getTimeZoneOffset() {
$timezone = $this->getTimeZone();
$now = new DateTime('@'.PhabricatorTime::getNow());
$offset = $timezone->getOffset($now);
// Javascript offsets are in minutes and have the opposite sign.
$offset = -(int)($offset / 60);
return $offset;
}
public function getTimeZoneOffsetInHours() {
$offset = $this->getTimeZoneOffset();
$offset = (int)round($offset / 60);
$offset = -$offset;
return $offset;
}
public function formatShortDateTime($when, $now = null) {
if ($now === null) {
$now = PhabricatorTime::getNow();
}
try {
$when = new DateTime('@'.$when);
$now = new DateTime('@'.$now);
} catch (Exception $ex) {
return null;
}
$zone = $this->getTimeZone();
$when->setTimeZone($zone);
$now->setTimeZone($zone);
if ($when->format('Y') !== $now->format('Y')) {
// Different year, so show "Feb 31 2075".
$format = 'M j Y';
} else if ($when->format('Ymd') !== $now->format('Ymd')) {
// Same year but different month and day, so show "Feb 31".
$format = 'M j';
} else {
// Same year, month and day so show a time of day.
$pref_time = PhabricatorTimeFormatSetting::SETTINGKEY;
$format = $this->getUserSetting($pref_time);
}
return $when->format($format);
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
public function getDefaultSpacePHID() {
// TODO: We might let the user switch which space they're "in" later on;
// for now just use the global space if one exists.
// If the viewer has access to the default space, use that.
$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
foreach ($spaces as $space) {
if ($space->getIsDefaultNamespace()) {
return $space->getPHID();
}
}
// Otherwise, use the space with the lowest ID that they have access to.
// This just tends to keep the default stable and predictable over time,
// so adding a new space won't change behavior for users.
if ($spaces) {
$spaces = msort($spaces, 'getID');
return head($spaces)->getPHID();
}
return null;
}
/**
* Grant a user a source of authority, to let them bypass policy checks they
* could not otherwise.
*/
public function grantAuthority($authority) {
$this->authorities[] = $authority;
return $this;
}
/**
* Get authorities granted to the user.
*/
public function getAuthorities() {
return $this->authorities;
}
public function hasConduitClusterToken() {
return ($this->conduitClusterToken !== self::ATTACHABLE);
}
public function attachConduitClusterToken(PhabricatorConduitToken $token) {
$this->conduitClusterToken = $token;
return $this;
}
public function getConduitClusterToken() {
return $this->assertAttached($this->conduitClusterToken);
}
/* -( Availability )------------------------------------------------------- */
/**
* @task availability
*/
public function attachAvailability(array $availability) {
$this->availability = $availability;
return $this;
}
/**
* Get the timestamp the user is away until, if they are currently away.
*
* @return int|null Epoch timestamp, or `null` if the user is not away.
* @task availability
*/
public function getAwayUntil() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'until');
}
public function getDisplayAvailability() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
return idx($availability, 'availability', $busy);
}
public function getAvailabilityEventPHID() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'eventPHID');
}
/**
* Get cached availability, if present.
*
* @return wild|null Cache data, or null if no cache is available.
* @task availability
*/
public function getAvailabilityCache() {
$now = PhabricatorTime::getNow();
if ($this->availabilityCacheTTL <= $now) {
return null;
}
try {
return phutil_json_decode($this->availabilityCache);
} catch (Exception $ex) {
return null;
}
}
/**
* Write to the availability cache.
*
* @param wild Availability cache data.
* @param int|null Cache TTL.
* @return this
* @task availability
*/
public function writeAvailabilityCache(array $availability, $ttl) {
if (PhabricatorEnv::isReadOnly()) {
return $this;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
WHERE id = %d',
$this->getTableName(),
phutil_json_encode($availability),
$ttl,
$this->getID());
unset($unguarded);
return $this;
}
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
// c4science custo
// Allow administrators to bypass all policies.
if ($this->getIsAdmin()) {
return true;
}
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
/**
* Get a scalar string identifying this user.
*
* This is similar to using the PHID, but distinguishes between omnipotent
* and public users explicitly. This allows safe construction of cache keys
* or cache buckets which do not conflate public and omnipotent users.
*
* @return string Scalar identifier.
*/
public function getCacheFragment() {
if ($this->isOmnipotent()) {
return 'u.omnipotent';
}
$phid = $this->getPHID();
if ($phid) {
return 'u.'.$phid;
}
return 'u.public';
}
/* -( Managing Handles )--------------------------------------------------- */
/**
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
* internal handle pool.
*
* @param list<phid> List of PHIDs to load.
* @return PhabricatorHandleList Handle list object.
* @task handle
*/
public function loadHandles(array $phids) {
if ($this->handlePool === null) {
$this->handlePool = id(new PhabricatorHandlePool())
->setViewer($this);
}
return $this->handlePool->newHandleList($phids);
}
/**
* Get a @{class:PHUIHandleView} for a single handle.
*
* This benefits from the viewer's internal handle pool.
*
* @param phid PHID to render a handle for.
* @return PHUIHandleView View of the handle.
* @task handle
*/
public function renderHandle($phid) {
return $this->loadHandles(array($phid))->renderHandle($phid);
}
/**
* Get a @{class:PHUIHandleListView} for a list of handles.
*
* This benefits from the viewer's internal handle pool.
*
* @param list<phid> List of PHIDs to render.
* @return PHUIHandleListView View of the handles.
* @task handle
*/
public function renderHandleList(array $phids) {
return $this->loadHandles($phids)->renderList();
}
public function attachBadgePHIDs(array $phids) {
$this->badgePHIDs = $phids;
return $this;
}
public function getBadgePHIDs() {
return $this->assertAttached($this->badgePHIDs);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$externals = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer($engine->getViewer())
->withUsers(array($this))
->execute();
foreach ($prefs as $pref) {
$engine->destroyObject($pref);
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($engine->getViewer())
->withObjectPHIDs(array($this->getPHID()))
->execute();
foreach ($keys as $key) {
$engine->destroyObject($key);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/user/'.$this->getUsername().'/page/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
public function getSSHKeyNotifyPHIDs() {
return array(
$this->getPHID(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
- return new PhabricatorUserProfileEditor();
+ return new PhabricatorUserTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorUserTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorUserFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorUserFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('username')
->setType('string')
->setDescription(pht("The user's username.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('realName')
->setType('string')
->setDescription(pht("The user's real name.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('roles')
->setType('list<string>')
->setDescription(pht('List of account roles.')),
);
}
public function getFieldValuesForConduit() {
$roles = array();
if ($this->getIsDisabled()) {
$roles[] = 'disabled';
}
if ($this->getIsSystemAgent()) {
$roles[] = 'bot';
}
if ($this->getIsMailingList()) {
$roles[] = 'list';
}
if ($this->getIsAdmin()) {
$roles[] = 'admin';
}
if ($this->getIsEmailVerified()) {
$roles[] = 'verified';
}
if ($this->getIsApproved()) {
$roles[] = 'approved';
}
if ($this->isUserActivated()) {
$roles[] = 'activated';
}
return array(
'username' => $this->getUsername(),
'realName' => $this->getRealName(),
'roles' => $roles,
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
public function attachRawCacheData(array $data) {
$this->rawCacheData = $data + $this->rawCacheData;
return $this;
}
public function setAllowInlineCacheGeneration($allow_cache_generation) {
$this->allowInlineCacheGeneration = $allow_cache_generation;
return $this;
}
/**
* @task cache
*/
protected function requireCacheData($key) {
if (isset($this->usableCacheData[$key])) {
return $this->usableCacheData[$key];
}
$type = PhabricatorUserCacheType::requireCacheTypeForKey($key);
if (isset($this->rawCacheData[$key])) {
$raw_value = $this->rawCacheData[$key];
$usable_value = $type->getValueFromStorage($raw_value);
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
// By default, we throw if a cache isn't available. This is consistent
// with the standard `needX()` + `attachX()` + `getX()` interaction.
if (!$this->allowInlineCacheGeneration) {
throw new PhabricatorDataNotAttachedException($this);
}
$user_phid = $this->getPHID();
// Try to read the actual cache before we generate a new value. We can
// end up here via Conduit, which does not use normal sessions and can
// not pick up a free cache load during session identification.
if ($user_phid) {
$raw_data = PhabricatorUserCache::readCaches(
$type,
$key,
array($user_phid));
if (array_key_exists($user_phid, $raw_data)) {
$raw_value = $raw_data[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
}
$usable_value = $type->getDefaultValue();
if ($user_phid) {
$map = $type->newValueForUsers($key, array($this));
if (array_key_exists($user_phid, $map)) {
$raw_value = $map[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
PhabricatorUserCache::writeCache(
$type,
$key,
$user_phid,
$raw_value);
}
}
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
/**
* @task cache
*/
public function clearCacheData($key) {
unset($this->rawCacheData[$key]);
unset($this->usableCacheData[$key]);
return $this;
}
public function getCSSValue($variable_key) {
$preference = PhabricatorAccessibilitySetting::SETTINGKEY;
$key = $this->getUserSetting($preference);
$postprocessor = CelerityPostprocessor::getPostprocessor($key);
$variables = $postprocessor->getVariables();
if (!isset($variables[$variable_key])) {
throw new Exception(
pht(
'Unknown CSS variable "%s"!',
$variable_key));
}
return $variables[$variable_key];
}
/* -( PhabricatorAuthPasswordHashInterface )------------------------------- */
public function newPasswordDigest(
PhutilOpaqueEnvelope $envelope,
PhabricatorAuthPassword $password) {
// Before passwords are hashed, they are digested. The goal of digestion
// is twofold: to reduce the length of very long passwords to something
// reasonable; and to salt the password in case the best available hasher
// does not include salt automatically.
// Users may choose arbitrarily long passwords, and attackers may try to
// attack the system by probing it with very long passwords. When large
// inputs are passed to hashers -- which are intentionally slow -- it
// can result in unacceptably long runtimes. The classic attack here is
// to try to log in with a 64MB password and see if that locks up the
// machine for the next century. By digesting passwords to a standard
// length first, the length of the raw input does not impact the runtime
// of the hashing algorithm.
// Some hashers like bcrypt are self-salting, while other hashers are not.
// Applying salt while digesting passwords ensures that hashes are salted
// whether we ultimately select a self-salting hasher or not.
// For legacy compatibility reasons, old VCS and Account password digest
// algorithms are significantly more complicated than necessary to achieve
// these goals. This is because they once used a different hashing and
// salting process. When we upgraded to the modern modular hasher
// infrastructure, we just bolted it onto the end of the existing pipelines
// so that upgrading didn't break all users' credentials.
// New implementations can (and, generally, should) safely select the
// simple HMAC SHA256 digest at the bottom of the function, which does
// everything that a digest callback should without any needless legacy
// baggage on top.
if ($password->getLegacyDigestFormat() == 'v1') {
switch ($password->getPasswordType()) {
case PhabricatorAuthPassword::PASSWORD_TYPE_VCS:
// Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm.
// They originally used this as a hasher, but it became a digest
// algorithm once hashing was upgraded to include bcrypt.
$digest = $envelope->openEnvelope();
$salt = $this->getPHID();
for ($ii = 0; $ii < 1000; $ii++) {
$digest = PhabricatorHash::weakDigest($digest, $salt);
}
return new PhutilOpaqueEnvelope($digest);
case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT:
// Account passwords previously used this weird mess of salt and did
// not digest the input to a standard length.
// Beyond this being a weird special case, there are two actual
// problems with this, although neither are particularly severe:
// First, because we do not normalize the length of passwords, this
// algorithm may make us vulnerable to DOS attacks where an attacker
// attempts to use a very long input to slow down hashers.
// Second, because the username is part of the hash algorithm,
// renaming a user breaks their password. This isn't a huge deal but
// it's pretty silly. There's no security justification for this
// behavior, I just didn't think about the implication when I wrote
// it originally.
$parts = array(
$this->getUsername(),
$envelope->openEnvelope(),
$this->getPHID(),
$password->getPasswordSalt(),
);
return new PhutilOpaqueEnvelope(implode('', $parts));
}
}
// For passwords which do not have some crazy legacy reason to use some
// other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies
// the digest requirements and is simple.
$digest = PhabricatorHash::digestHMACSHA256(
$envelope->openEnvelope(),
$password->getPasswordSalt());
return new PhutilOpaqueEnvelope($digest);
}
}
diff --git a/src/applications/people/storage/PhabricatorUserTransaction.php b/src/applications/people/storage/PhabricatorUserTransaction.php
index b4bd1c986..24edb2f5b 100644
--- a/src/applications/people/storage/PhabricatorUserTransaction.php
+++ b/src/applications/people/storage/PhabricatorUserTransaction.php
@@ -1,18 +1,22 @@
<?php
final class PhabricatorUserTransaction
- extends PhabricatorApplicationTransaction {
+ extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'user';
}
public function getApplicationTransactionType() {
return PhabricatorPeopleUserPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
+ public function getBaseTransactionClass() {
+ return 'PhabricatorUserTransactionType';
+ }
+
}
diff --git a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php
new file mode 100644
index 000000000..ab399a28e
--- /dev/null
+++ b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php
@@ -0,0 +1,86 @@
+<?php
+
+final class PhabricatorUserDisableTransaction
+ extends PhabricatorUserTransactionType {
+
+ const TRANSACTIONTYPE = 'user.disable';
+
+ public function generateOldValue($object) {
+ return (bool)$object->getIsDisabled();
+ }
+
+ public function generateNewValue($object, $value) {
+ return (bool)$value;
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setIsDisabled((int)$value);
+
+ $this->newUserLog(PhabricatorUserLog::ACTION_DISABLE)
+ ->setOldValue((bool)$object->getIsDisabled())
+ ->setNewValue((bool)$value)
+ ->save();
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+ if ($new) {
+ return pht(
+ '%s disabled this user.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s enabled this user.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function getTitleForFeed() {
+ $new = $this->getNewValue();
+ if ($new) {
+ return pht(
+ '%s disabled %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ } else {
+ return pht(
+ '%s enabled %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $is_disabled = (bool)$object->getIsDisabled();
+
+ if ((bool)$xaction->getNewValue() === $is_disabled) {
+ continue;
+ }
+
+ // You must have the "Can Disable Users" permission to disable a user.
+ $this->requireApplicationCapability(
+ PeopleDisableUsersCapability::CAPABILITY);
+
+ if ($this->getActingAsPHID() === $object->getPHID()) {
+ $errors[] = $this->newInvalidError(
+ pht('You can not enable or disable your own account.'));
+ }
+ }
+
+ return $errors;
+ }
+
+ public function getRequiredCapabilities(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+
+ // You do not need to be able to edit users to disable them. Instead, this
+ // requirement is replaced with a requirement that you have the "Can
+ // Disable Users" permission.
+
+ return null;
+ }
+}
diff --git a/src/applications/people/xaction/PhabricatorUserTransactionType.php b/src/applications/people/xaction/PhabricatorUserTransactionType.php
new file mode 100644
index 000000000..dcd45d480
--- /dev/null
+++ b/src/applications/people/xaction/PhabricatorUserTransactionType.php
@@ -0,0 +1,13 @@
+<?php
+
+abstract class PhabricatorUserTransactionType
+ extends PhabricatorModularTransactionType {
+
+ protected function newUserLog($action) {
+ return PhabricatorUserLog::initializeNewLog(
+ $this->getActor(),
+ $this->getObject()->getPHID(),
+ $action);
+ }
+
+}
diff --git a/src/applications/phame/controller/PhameHomeController.php b/src/applications/phame/controller/PhameHomeController.php
index 037026fa8..bf263e97a 100644
--- a/src/applications/phame/controller/PhameHomeController.php
+++ b/src/applications/phame/controller/PhameHomeController.php
@@ -1,132 +1,132 @@
<?php
final class PhameHomeController extends PhamePostController {
public function shouldAllowPublic() {
return true;
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
id(new PhameBlogEditEngine())
->setViewer($this->getViewer())
->addActionToCrumbs($crumbs);
return $crumbs;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$blogs = id(new PhameBlogQuery())
->setViewer($viewer)
->withStatuses(array(PhameBlog::STATUS_ACTIVE))
->needProfileImage(true)
->execute();
$post_list = null;
if ($blogs) {
$blog_phids = mpull($blogs, 'getPHID');
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$posts = id(new PhamePostQuery())
->setViewer($viewer)
->withBlogPHIDs($blog_phids)
->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED))
->setOrder('datePublished')
->executeWithCursorPager($pager);
if ($posts) {
$post_list = id(new PhamePostListView())
->setPosts($posts)
->setViewer($viewer)
->showBlog(true);
} else {
$post_list = id(new PHUIBigInfoView())
->setIcon('fa-star')
->setTitle('No Visible Posts')
->setDescription(
pht('There aren\'t any visible blog posts.'));
}
} else {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Blog'))
->setHref('/phame/blog/edit/')
->setColor(PHUIButtonView::GREEN);
$post_list = id(new PHUIBigInfoView())
->setIcon('fa-star')
->setTitle('Welcome to Phame')
->setDescription(
pht('There aren\'t any visible blog posts.'))
->addAction($create_button);
}
$view_all = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View All'))
->setHref($this->getApplicationURI('post/'))
->setIcon('fa-list-ul');
$title = pht('Recent Posts');
$header = id(new PHUIHeaderView())
->setHeader($title)
->addActionLink($view_all);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb(
pht('Recent Posts'),
$this->getApplicationURI('post/'));
- $page = id(new PHUIDocumentViewPro())
+ $page = id(new PHUIDocumentView())
->setHeader($header)
->appendChild($post_list);
$blog_list = id(new PhameBlogListView())
->setBlogs($blogs)
->setViewer($viewer);
$draft_list = null;
if ($viewer->isLoggedIn() && $blogs) {
$drafts = id(new PhamePostQuery())
->setViewer($viewer)
->withBloggerPHIDs(array($viewer->getPHID()))
->withBlogPHIDs(mpull($blogs, 'getPHID'))
->withVisibility(array(PhameConstants::VISIBILITY_DRAFT))
->setLimit(5)
->execute();
$draft_list = id(new PhameDraftListView())
->setPosts($drafts)
->setBlogs($blogs)
->setViewer($viewer);
}
$phame_view = id(new PHUITwoColumnView())
->setMainColumn(array(
$page,
))
->setSideColumn(array(
$blog_list,
$draft_list,
))
->addClass('phame-home-container');
$phame_home = phutil_tag_div('phame-home-view', $phame_view);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$phame_home,
));
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php
index 08b23e5a9..e9e422aeb 100644
--- a/src/applications/phame/controller/blog/PhameBlogViewController.php
+++ b/src/applications/phame/controller/blog/PhameBlogViewController.php
@@ -1,195 +1,195 @@
<?php
final class PhameBlogViewController extends PhameLiveController {
public function handleRequest(AphrontRequest $request) {
$response = $this->setupLiveEnvironment();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$blog = $this->getBlog();
$is_live = $this->getIsLive();
$is_external = $this->getIsExternal();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$post_query = id(new PhamePostQuery())
->setViewer($viewer)
->withBlogPHIDs(array($blog->getPHID()))
->setOrder('datePublished')
->withVisibility(array(
PhameConstants::VISIBILITY_PUBLISHED,
PhameConstants::VISIBILITY_DRAFT,
));
if ($is_live) {
$post_query->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED));
}
$posts = $post_query->executeWithCursorPager($pager);
$hero = $this->buildPhameHeader($blog);
$header = id(new PHUIHeaderView())
->addClass('phame-header-bar')
->setUser($viewer);
if (!$is_external) {
if ($blog->isArchived()) {
$header_icon = 'fa-ban';
$header_name = pht('Archived');
$header_color = 'dark';
} else {
$header_icon = 'fa-check';
$header_name = pht('Active');
$header_color = 'bluegrey';
}
$header->setStatus($header_icon, $header_color, $header_name);
$actions = $this->renderActions($blog);
$header->setActionList($actions);
$header->setPolicyObject($blog);
}
if ($posts) {
$post_list = id(new PhamePostListView())
->setPosts($posts)
->setViewer($viewer)
->setIsExternal($is_external)
->setIsLive($is_live)
->setNodata(pht('This blog has no visible posts.'));
} else {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Write a Post'))
->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID()))
->setColor(PHUIButtonView::GREEN);
$post_list = id(new PHUIBigInfoView())
->setIcon('fa-star')
->setTitle($blog->getName())
->setDescription(
pht('No one has written any blog posts yet.'));
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$blog,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$post_list->addAction($create_button);
}
}
- $page = id(new PHUIDocumentViewPro())
+ $page = id(new PHUIDocumentView())
->setHeader($header)
->appendChild($post_list);
$description = null;
if (strlen($blog->getDescription())) {
$description = new PHUIRemarkupView(
$viewer,
$blog->getDescription());
} else {
$description = phutil_tag('em', array(), pht('No description.'));
}
$about = id(new PhameDescriptionView())
->setTitle(pht('About %s', $blog->getName()))
->setDescription($description)
->setImage($blog->getProfileImageURI());
$crumbs = $this->buildApplicationCrumbs();
$page = $this->newPage()
->setTitle($blog->getName())
->setPageObjectPHIDs(array($blog->getPHID()))
->setCrumbs($crumbs)
->appendChild(
array(
$hero,
$page,
$about,
));
return $page;
}
private function renderActions(PhameBlog $blog) {
$viewer = $this->getViewer();
$actions = id(new PhabricatorActionListView())
->setObject($blog)
->setUser($viewer);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$blog,
PhabricatorPolicyCapability::CAN_EDIT);
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-plus')
->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID()))
->setName(pht('Write Post'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
->setIcon('fa-search')
->setHref(
$this->getApplicationURI('post/?blog='.$blog->getPHID()))
->setName(pht('Search Posts')));
$actions->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
->setIcon('fa-globe')
->setHref($blog->getLiveURI())
->setName(pht('View Live')));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref($this->getApplicationURI('blog/manage/'.$blog->getID().'/'))
->setName(pht('Manage Blog')));
return $actions;
}
private function buildPhameHeader(
PhameBlog $blog) {
$image = null;
if ($blog->getHeaderImagePHID()) {
$image = phutil_tag(
'div',
array(
'class' => 'phame-header-hero',
),
phutil_tag(
'img',
array(
'src' => $blog->getHeaderImageURI(),
'class' => 'phame-header-image',
)));
}
$title = phutil_tag_div('phame-header-title', $blog->getName());
$subtitle = null;
if ($blog->getSubtitle()) {
$subtitle = phutil_tag_div('phame-header-subtitle', $blog->getSubtitle());
}
return phutil_tag_div(
'phame-mega-header', array($image, $title, $subtitle));
}
}
diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php
index 63adedb7a..11d94d2f9 100644
--- a/src/applications/phame/controller/post/PhamePostViewController.php
+++ b/src/applications/phame/controller/post/PhamePostViewController.php
@@ -1,353 +1,353 @@
<?php
final class PhamePostViewController
extends PhameLiveController {
public function handleRequest(AphrontRequest $request) {
$response = $this->setupLiveEnvironment();
if ($response) {
return $response;
}
$viewer = $request->getViewer();
$moved = $request->getStr('moved');
$post = $this->getPost();
$blog = $this->getBlog();
$is_live = $this->getIsLive();
$is_external = $this->getIsExternal();
$header = id(new PHUIHeaderView())
->addClass('phame-header-bar')
->setUser($viewer);
$hero = $this->buildPhamePostHeader($post);
if (!$is_external) {
$actions = $this->renderActions($post);
$header->setPolicyObject($post);
$header->setActionList($actions);
}
- $document = id(new PHUIDocumentViewPro())
+ $document = id(new PHUIDocumentView())
->setHeader($header);
if ($moved) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('Post moved successfully.')));
}
if ($post->isDraft()) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Draft Post'))
->appendChild(
pht(
'This is a draft, and is only visible to you and other users '.
'who can edit %s. Use "Publish" to publish this post.',
$viewer->renderHandle($post->getBlogPHID()))));
}
if ($post->isArchived()) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_ERROR)
->setTitle(pht('Archived Post'))
->appendChild(
pht(
'This post has been archived, and is only visible to you and '.
'other users who can edit %s.',
$viewer->renderHandle($post->getBlogPHID()))));
}
if (!$post->getBlog()) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Not On A Blog'))
->appendChild(
pht('This post is not associated with a blog (the blog may have '.
'been deleted). Use "Move Post" to move it to a new blog.')));
}
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->addObject($post, PhamePost::MARKUP_FIELD_BODY)
->process();
$document->appendChild(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY)));
$blogger = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($post->getBloggerPHID()))
->needProfileImage(true)
->executeOne();
$blogger_profile = $blogger->loadUserProfile();
$author_uri = '/p/'.$blogger->getUsername().'/';
$author_uri = PhabricatorEnv::getURI($author_uri);
$author = phutil_tag(
'a',
array(
'href' => $author_uri,
),
$blogger->getUsername());
$date = phabricator_datetime($post->getDatePublished(), $viewer);
if ($post->isDraft()) {
$subtitle = pht('Unpublished draft by %s.', $author);
} else if ($post->isArchived()) {
$subtitle = pht('Archived post by %s.', $author);
} else {
$subtitle = pht('Written by %s on %s.', $author, $date);
}
$user_icon = $blogger_profile->getIcon();
$user_icon = PhabricatorPeopleIconSet::getIconIcon($user_icon);
$user_icon = id(new PHUIIconView())->setIcon($user_icon);
$about = id(new PhameDescriptionView())
->setTitle($subtitle)
->setDescription(
array(
$user_icon,
' ',
$blogger_profile->getDisplayTitle(),
))
->setImage($blogger->getProfileImageURI())
->setImageHref($author_uri);
$monogram = $post->getMonogram();
$timeline = $this->buildTransactionTimeline(
$post,
id(new PhamePostTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)));
$timeline->setQuoteRef($monogram);
if ($is_external) {
$add_comment = null;
} else {
$add_comment = $this->buildCommentForm($post, $timeline);
$add_comment = phutil_tag_div('mlb mlt phame-comment-view', $add_comment);
}
$timeline = phutil_tag_div('phui-document-view-pro-box', $timeline);
list($prev, $next) = $this->loadAdjacentPosts($post);
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($post);
$is_live = $this->getIsLive();
$is_external = $this->getIsExternal();
$next_view = new PhameNextPostView();
if ($next) {
$next_view->setNext($next->getTitle(),
$next->getBestURI($is_live, $is_external));
}
if ($prev) {
$next_view->setPrevious($prev->getTitle(),
$prev->getBestURI($is_live, $is_external));
}
$document->setFoot($next_view);
$crumbs = $this->buildApplicationCrumbs();
$properties = phutil_tag_div('phui-document-view-pro-box', $properties);
$page = $this->newPage()
->setTitle($post->getTitle())
->setPageObjectPHIDs(array($post->getPHID()))
->setCrumbs($crumbs)
->appendChild(
array(
$hero,
$document,
$about,
$properties,
$timeline,
$add_comment,
));
if ($is_live) {
$page
->setShowChrome(false)
->setShowFooter(false);
}
return $page;
}
private function renderActions(PhamePost $post) {
$viewer = $this->getViewer();
$actions = id(new PhabricatorActionListView())
->setObject($post)
->setUser($viewer);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$post,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $post->getID();
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref($this->getApplicationURI('post/edit/'.$id.'/'))
->setName(pht('Edit Post'))
->setDisabled(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-camera-retro')
->setHref($this->getApplicationURI('post/header/'.$id.'/'))
->setName(pht('Edit Header Image'))
->setDisabled(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-arrows')
->setHref($this->getApplicationURI('post/move/'.$id.'/'))
->setName(pht('Move Post'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-history')
->setHref($this->getApplicationURI('post/history/'.$id.'/'))
->setName(pht('View History')));
if ($post->isDraft()) {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye')
->setHref($this->getApplicationURI('post/publish/'.$id.'/'))
->setName(pht('Publish'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setHref($this->getApplicationURI('post/archive/'.$id.'/'))
->setName(pht('Archive'))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else if ($post->isArchived()) {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye')
->setHref($this->getApplicationURI('post/publish/'.$id.'/'))
->setName(pht('Publish'))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye-slash')
->setHref($this->getApplicationURI('post/unpublish/'.$id.'/'))
->setName(pht('Unpublish'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setHref($this->getApplicationURI('post/archive/'.$id.'/'))
->setName(pht('Archive'))
->setDisabled(!$can_edit)
->setWorkflow(true));
}
if ($post->isDraft()) {
$live_name = pht('Preview');
} else {
$live_name = pht('View Live');
}
if (!$post->isArchived()) {
$actions->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
->setIcon('fa-globe')
->setHref($post->getLiveURI())
->setName($live_name));
}
return $actions;
}
private function buildCommentForm(PhamePost $post, $timeline) {
$viewer = $this->getViewer();
$box = id(new PhamePostEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($post)
->setTransactionTimeline($timeline);
return phutil_tag_div('phui-document-view-pro-box', $box);
}
private function loadAdjacentPosts(PhamePost $post) {
$viewer = $this->getViewer();
$query = id(new PhamePostQuery())
->setViewer($viewer)
->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED))
->withBlogPHIDs(array($post->getBlog()->getPHID()))
->setLimit(1);
$prev = id(clone $query)
->setAfterID($post->getID())
->execute();
$next = id(clone $query)
->setBeforeID($post->getID())
->execute();
return array(head($prev), head($next));
}
private function buildPhamePostHeader(
PhamePost $post) {
$image = null;
if ($post->getHeaderImagePHID()) {
$image = phutil_tag(
'div',
array(
'class' => 'phame-header-hero',
),
phutil_tag(
'img',
array(
'src' => $post->getHeaderImageURI(),
'class' => 'phame-header-image',
)));
}
$title = phutil_tag_div('phame-header-title', $post->getTitle());
$subtitle = null;
if ($post->getSubtitle()) {
$subtitle = phutil_tag_div('phame-header-subtitle', $post->getSubtitle());
}
return phutil_tag_div(
'phame-mega-header', array($image, $title, $subtitle));
}
}
diff --git a/src/applications/phid/view/PHUIHandleListView.php b/src/applications/phid/view/PHUIHandleListView.php
index d15b3d15c..24104fe76 100644
--- a/src/applications/phid/view/PHUIHandleListView.php
+++ b/src/applications/phid/view/PHUIHandleListView.php
@@ -1,87 +1,102 @@
<?php
/**
* Convenience class for rendering a list of handles.
*
* This class simplifies rendering a list of handles and improves loading and
* caching semantics in the rendering pipeline by delaying bulk loads until the
* last possible moment.
*/
final class PHUIHandleListView
extends AphrontTagView {
private $handleList;
private $asInline;
private $asText;
private $showStateIcons;
+ private $glyphLimit;
public function setHandleList(PhabricatorHandleList $list) {
$this->handleList = $list;
return $this;
}
public function setAsInline($inline) {
$this->asInline = $inline;
return $this;
}
public function getAsInline() {
return $this->asInline;
}
public function setAsText($as_text) {
$this->asText = $as_text;
return $this;
}
public function getAsText() {
return $this->asText;
}
public function setShowStateIcons($show_state_icons) {
$this->showStateIcons = $show_state_icons;
return $this;
}
public function getShowStateIcons() {
return $this->showStateIcons;
}
+ public function setGlyphLimit($glyph_limit) {
+ $this->glyphLimit = $glyph_limit;
+ return $this;
+ }
+
+ public function getGlyphLimit() {
+ return $this->glyphLimit;
+ }
+
protected function getTagName() {
if ($this->getAsText()) {
return null;
} else {
// TODO: It would be nice to render this with a proper <ul />, at least
// in block mode, but don't stir the waters up too much for now.
return 'span';
}
}
protected function getTagContent() {
$list = $this->handleList;
$show_state_icons = $this->getShowStateIcons();
+ $glyph_limit = $this->getGlyphLimit();
$items = array();
foreach ($list as $handle) {
$view = $list->renderHandle($handle->getPHID())
->setShowHovercard(true)
->setAsText($this->getAsText());
if ($show_state_icons) {
$view->setShowStateIcon(true);
}
+ if ($glyph_limit) {
+ $view->setGlyphLimit($glyph_limit);
+ }
+
$items[] = $view;
}
if ($this->getAsInline()) {
$items = phutil_implode_html(', ', $items);
} else {
$items = phutil_implode_html(phutil_tag('br'), $items);
}
return $items;
}
}
diff --git a/src/applications/phid/view/PHUIHandleView.php b/src/applications/phid/view/PHUIHandleView.php
index 96ae7de02..fe3c62a9a 100644
--- a/src/applications/phid/view/PHUIHandleView.php
+++ b/src/applications/phid/view/PHUIHandleView.php
@@ -1,98 +1,115 @@
<?php
/**
* Convenience class for rendering a single handle.
*
* This class simplifies rendering a single handle, and improves loading and
* caching semantics in the rendering pipeline by loading data at the last
* moment.
*/
final class PHUIHandleView
extends AphrontView {
private $handleList;
private $handlePHID;
private $asTag;
private $asText;
private $useShortName;
private $showHovercard;
private $showStateIcon;
+ private $glyphLimit;
public function setHandleList(PhabricatorHandleList $list) {
$this->handleList = $list;
return $this;
}
public function setHandlePHID($phid) {
$this->handlePHID = $phid;
return $this;
}
public function setAsTag($tag) {
$this->asTag = $tag;
return $this;
}
public function setAsText($as_text) {
$this->asText = $as_text;
return $this;
}
public function setUseShortName($short) {
$this->useShortName = $short;
return $this;
}
public function setShowHovercard($hovercard) {
$this->showHovercard = $hovercard;
return $this;
}
public function setShowStateIcon($show_state_icon) {
$this->showStateIcon = $show_state_icon;
return $this;
}
public function getShowStateIcon() {
return $this->showStateIcon;
}
+ public function setGlyphLimit($glyph_limit) {
+ $this->glyphLimit = $glyph_limit;
+ return $this;
+ }
+
+ public function getGlyphLimit() {
+ return $this->glyphLimit;
+ }
+
public function render() {
$handle = $this->handleList[$this->handlePHID];
if ($this->asTag) {
$tag = $handle->renderTag();
if ($this->showHovercard) {
$tag->setPHID($handle->getPHID());
}
return $tag;
}
if ($this->asText) {
return $handle->getLinkName();
}
if ($this->useShortName) {
$name = $handle->getName();
} else {
- $name = null;
+ $name = $handle->getLinkName();
+ }
+
+ $glyph_limit = $this->getGlyphLimit();
+ if ($glyph_limit) {
+ $name = id(new PhutilUTF8StringTruncator())
+ ->setMaximumGlyphs($glyph_limit)
+ ->truncateString($name);
}
if ($this->showHovercard) {
$link = $handle->renderHovercardLink($name);
} else {
$link = $handle->renderLink($name);
}
if ($this->showStateIcon) {
$icon = $handle->renderStateIcon();
$link = array($icon, ' ', $link);
}
return $link;
}
}
diff --git a/src/applications/phlux/storage/PhluxSchemaSpec.php b/src/applications/phlux/storage/PhluxSchemaSpec.php
new file mode 100644
index 000000000..ee8a38b6f
--- /dev/null
+++ b/src/applications/phlux/storage/PhluxSchemaSpec.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhluxSchemaSpec
+ extends PhabricatorConfigSchemaSpec {
+
+ public function buildSchemata() {
+ $this->buildEdgeSchemata(new PhluxVariable());
+ }
+
+}
diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php
index 1712d3f97..8937d6ee8 100644
--- a/src/applications/phortune/storage/PhortunePaymentMethod.php
+++ b/src/applications/phortune/storage/PhortunePaymentMethod.php
@@ -1,161 +1,165 @@
<?php
/**
* A payment method is a credit card; it is associated with an account and
* charges can be made against it.
*/
final class PhortunePaymentMethod extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_ACTIVE = 'payment:active';
const STATUS_DISABLED = 'payment:disabled';
protected $name = '';
protected $status;
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $providerPHID;
protected $expires;
protected $metadata = array();
protected $brand;
protected $lastFourDigits;
private $account = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
private $providerConfig = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'status' => 'text64',
'brand' => 'text64',
'expires' => 'text16',
'lastFourDigits' => 'text16',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID', 'status'),
),
'key_merchant' => array(
'columns' => array('merchantPHID', 'accountPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortunePaymentMethodPHIDType::TYPECONST);
}
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 attachProviderConfig(PhortunePaymentProviderConfig $config) {
$this->providerConfig = $config;
return $this;
}
public function getProviderConfig() {
return $this->assertAttached($this->providerConfig);
}
public function getDescription() {
$provider = $this->buildPaymentProvider();
- return $provider->getPaymentMethodProviderDescription();
+
+ $expires = $this->getDisplayExpires();
+ $description = $provider->getPaymentMethodProviderDescription();
+
+ return pht("Expires %s \xC2\xB7 %s", $expires, $description);
}
public function getMetadataValue($key, $default = null) {
return idx($this->getMetadata(), $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function buildPaymentProvider() {
return $this->getProviderConfig()->buildProvider();
}
public function getDisplayName() {
if (strlen($this->name)) {
return $this->name;
}
$provider = $this->buildPaymentProvider();
return $provider->getDefaultPaymentMethodDisplayName($this);
}
public function getFullDisplayName() {
return pht('%s (%s)', $this->getDisplayName(), $this->getSummary());
}
public function getSummary() {
return pht('%s %s', $this->getBrand(), $this->getLastFourDigits());
}
public function setExpires($year, $month) {
$this->expires = $year.'-'.$month;
return $this;
}
public function getDisplayExpires() {
list($year, $month) = explode('-', $this->getExpires());
$month = sprintf('%02d', $month);
$year = substr($year, -2);
return $month.'/'.$year;
}
public function isActive() {
return ($this->getStatus() === self::STATUS_ACTIVE);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getAccount()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getAccount()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Members of an account can always view and edit its payment methods.');
}
}
diff --git a/src/applications/phriction/application/PhabricatorPhrictionApplication.php b/src/applications/phriction/application/PhabricatorPhrictionApplication.php
index ea91727c7..d44845c4b 100644
--- a/src/applications/phriction/application/PhabricatorPhrictionApplication.php
+++ b/src/applications/phriction/application/PhabricatorPhrictionApplication.php
@@ -1,78 +1,80 @@
<?php
final class PhabricatorPhrictionApplication extends PhabricatorApplication {
public function getName() {
return pht('Phriction');
}
public function getShortDescription() {
return pht('Wiki Documents');
}
public function getBaseURI() {
return '/w/';
}
public function getIcon() {
return 'fa-book';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Phriction User Guide'),
'href' => PhabricatorEnv::getDoclink('Phriction User Guide'),
),
);
}
public function getTitleGlyph() {
return "\xE2\x9A\xA1";
}
public function getRemarkupRules() {
return array(
new PhrictionRemarkupRule(),
);
}
public function getRoutes() {
return array(
// Match "/w/" with slug "/".
'/w(?P<slug>/)' => 'PhrictionDocumentController',
// Match "/w/x/y/z/" with slug "x/y/z/".
'/w/(?P<slug>.+/)' => 'PhrictionDocumentController',
'/phriction/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhrictionListController',
'history(?P<slug>/)' => 'PhrictionHistoryController',
'history/(?P<slug>.+/)' => 'PhrictionHistoryController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'PhrictionEditController',
'delete/(?P<id>[1-9]\d*)/' => 'PhrictionDeleteController',
+ 'publish/(?P<documentID>[1-9]\d*)/(?P<contentID>[1-9]\d*)/'
+ => 'PhrictionPublishController',
'new/' => 'PhrictionNewController',
'move/(?P<id>[1-9]\d*)/' => 'PhrictionMoveController',
'preview/(?P<slug>.*/)' => 'PhrictionMarkupPreviewController',
'diff/(?P<id>[1-9]\d*)/' => 'PhrictionDiffController',
),
);
}
public function getApplicationOrder() {
return 0.140;
}
public function getApplicationSearchDocumentTypes() {
return array(
PhrictionDocumentPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php b/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php
new file mode 100644
index 000000000..748bec705
--- /dev/null
+++ b/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php
@@ -0,0 +1,82 @@
+<?php
+
+final class PhrictionDocumentPolicyCodex
+ extends PhabricatorPolicyCodex {
+
+ public function getPolicySpecialRuleDescriptions() {
+ $object = $this->getObject();
+ $strongest_policy = $this->getStrongestPolicy();
+
+ $rules = array();
+ $rules[] = $this->newRule()
+ ->setDescription(
+ pht('To view a wiki document, you must also be able to view all '.
+ 'of its ancestors. The most-restrictive view policy of this '.
+ 'document\'s ancestors is "%s".',
+ $strongest_policy->getShortName()))
+ ->setCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW));
+
+ $rules[] = $this->newRule()
+ ->setDescription(
+ pht('To edit a wiki document, you must also be able to view all '.
+ 'of its ancestors.'))
+ ->setCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT));
+
+ return $rules;
+ }
+
+ public function getDefaultPolicy() {
+ $ancestors = $this->getObject()->getAncestors();
+ if ($ancestors) {
+ $root = head($ancestors);
+ } else {
+ $root = $this->getObject();
+ }
+
+ $root_policy_phid = $root->getPolicy($this->getCapability());
+
+ return id(new PhabricatorPolicyQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs(array($root_policy_phid))
+ ->executeOne();
+ }
+
+ public function compareToDefaultPolicy(PhabricatorPolicy $policy) {
+ $root_policy = $this->getDefaultPolicy();
+ $strongest_policy = $this->getStrongestPolicy();
+
+ // Note that we never return 'weaker', because Phriction documents can
+ // never have weaker permissions than their parents. If this object has
+ // been set to weaker permissions anyway, return 'adjusted'.
+ if ($root_policy == $strongest_policy) {
+ $strength = null;
+ } else if ($strongest_policy->isStrongerThan($root_policy)) {
+ $strength = PhabricatorPolicyStrengthConstants::STRONGER;
+ } else {
+ $strength = PhabricatorPolicyStrengthConstants::ADJUSTED;
+ }
+ return $strength;
+ }
+
+ private function getStrongestPolicy() {
+ $ancestors = $this->getObject()->getAncestors();
+ $ancestors[] = $this->getObject();
+
+ $strongest_policy = $this->getDefaultPolicy();
+ foreach ($ancestors as $ancestor) {
+ $ancestor_policy_phid = $ancestor->getPolicy($this->getCapability());
+
+ $ancestor_policy = id(new PhabricatorPolicyQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs(array($ancestor_policy_phid))
+ ->executeOne();
+
+ if ($ancestor_policy->isStrongerThan($strongest_policy)) {
+ $strongest_policy = $ancestor_policy;
+ }
+ }
+
+ return $strongest_policy;
+ }
+
+}
diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php
index f443ad657..c81a90a3d 100644
--- a/src/applications/phriction/controller/PhrictionDiffController.php
+++ b/src/applications/phriction/controller/PhrictionDiffController.php
@@ -1,258 +1,259 @@
<?php
final class PhrictionDiffController extends PhrictionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$document = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withIDs(array($id))
->needContent(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$current = $document->getContent();
$l = $request->getInt('l');
$r = $request->getInt('r');
$ref = $request->getStr('ref');
if ($ref) {
list($l, $r) = explode(',', $ref);
}
$content = id(new PhrictionContentQuery())
->setViewer($viewer)
->withDocumentPHIDs(array($document->getPHID()))
->withVersions(array($l, $r))
->execute();
$content = mpull($content, null, 'getVersion');
$content_l = idx($content, $l, null);
$content_r = idx($content, $r, null);
if (!$content_l || !$content_r) {
return new Aphront404Response();
}
$text_l = $content_l->getContent();
$text_r = $content_r->getContent();
$diff_view = id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setOldText($text_l)
->setNewText($text_r);
$changes = id(new PHUIObjectBoxView())
->setHeaderText(pht('Content Changes'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild(
phutil_tag(
'div',
array(
'class' => 'prose-diff-frame',
),
$diff_view));
require_celerity_resource('phriction-document-css');
$slug = $document->getSlug();
- $revert_l = $this->renderRevertButton($content_l, $current);
- $revert_r = $this->renderRevertButton($content_r, $current);
+ $revert_l = $this->renderRevertButton($document, $content_l, $current);
+ $revert_r = $this->renderRevertButton($document, $content_r, $current);
$crumbs = $this->buildApplicationCrumbs();
$crumb_views = $this->renderBreadcrumbs($slug);
foreach ($crumb_views as $view) {
$crumbs->addCrumb($view);
}
$crumbs->addTextCrumb(
pht('History'),
PhrictionDocument::getSlugURI($slug, 'history'));
$title = pht('Version %s vs %s', $l, $r);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-history');
$crumbs->addTextCrumb($title, $request->getRequestURI());
$comparison_table = $this->renderComparisonTable(
array(
$content_r,
$content_l,
));
$navigation_table = null;
if ($l + 1 == $r) {
$nav_l = ($l > 1);
$nav_r = ($r != $current->getVersion());
$uri = $request->getRequestURI();
if ($nav_l) {
$link_l = phutil_tag(
'a',
array(
'href' => $uri->alter('l', $l - 1)->alter('r', $r - 1),
'class' => 'button button-grey',
),
pht("\xC2\xAB Previous Change"));
} else {
$link_l = phutil_tag(
'a',
array(
'href' => '#',
'class' => 'button button-grey disabled',
),
pht('Original Change'));
}
$link_r = null;
if ($nav_r) {
$link_r = phutil_tag(
'a',
array(
'href' => $uri->alter('l', $l + 1)->alter('r', $r + 1),
'class' => 'button button-grey',
),
pht("Next Change \xC2\xBB"));
} else {
$link_r = phutil_tag(
'a',
array(
'href' => '#',
'class' => 'button button-grey disabled',
),
pht('Most Recent Change'));
}
$navigation_table = phutil_tag(
'table',
array('class' => 'phriction-history-nav-table'),
phutil_tag('tr', array(), array(
phutil_tag('td', array('class' => 'nav-prev'), $link_l),
phutil_tag('td', array('class' => 'nav-next'), $link_r),
)));
}
$output = hsprintf(
'<div class="phriction-document-history-diff">'.
'%s%s'.
'<table class="phriction-revert-table">'.
'<tr><td>%s</td><td>%s</td>'.
'</table>'.
'</div>',
$comparison_table->render(),
$navigation_table,
$revert_l,
$revert_r);
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Edits'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($output);
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$object_box,
$changes,
));
return $this->newPage()
->setTitle(pht('Document History'))
->setCrumbs($crumbs)
->appendChild($view);
}
private function renderRevertButton(
+ PhrictionDocument $document,
PhrictionContent $content,
PhrictionContent $current) {
- $document_id = $content->getDocumentID();
+ $document_id = $document->getID();
$version = $content->getVersion();
$hidden_statuses = array(
PhrictionChangeType::CHANGE_DELETE => true, // Silly
PhrictionChangeType::CHANGE_MOVE_AWAY => true, // Plain silly
PhrictionChangeType::CHANGE_STUB => true, // Utterly silly
);
if (isset($hidden_statuses[$content->getChangeType()])) {
// Don't show an edit/revert button for changes which deleted, moved or
// stubbed the content since it's silly.
return null;
}
if ($content->getID() == $current->getID()) {
return phutil_tag(
'a',
array(
'href' => '/phriction/edit/'.$document_id.'/',
'class' => 'button button-grey',
),
pht('Edit Current Version'));
}
return phutil_tag(
'a',
array(
'href' => '/phriction/edit/'.$document_id.'/?revert='.$version,
'class' => 'button button-grey',
),
pht('Revert to Version %s...', $version));
}
private function renderComparisonTable(array $content) {
assert_instances_of($content, 'PhrictionContent');
$viewer = $this->getViewer();
$phids = mpull($content, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$list = new PHUIObjectItemListView();
$first = true;
foreach ($content as $c) {
$author = $handles[$c->getAuthorPHID()]->renderLink();
$item = id(new PHUIObjectItemView())
->setHeader(pht('%s by %s, %s',
PhrictionChangeType::getChangeTypeLabel($c->getChangeType()),
$author,
pht('Version %s', $c->getVersion())))
->addAttribute(pht('%s %s',
phabricator_date($c->getDateCreated(), $viewer),
phabricator_time($c->getDateCreated(), $viewer)));
if ($c->getDescription()) {
$item->addAttribute($c->getDescription());
}
if ($first == true) {
$item->setStatusIcon('fa-file green');
$first = false;
} else {
$item->setStatusIcon('fa-file red');
}
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php
index 8377cf7d2..f2cf97450 100644
--- a/src/applications/phriction/controller/PhrictionDocumentController.php
+++ b/src/applications/phriction/controller/PhrictionDocumentController.php
@@ -1,498 +1,640 @@
<?php
final class PhrictionDocumentController
extends PhrictionController {
private $slug;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$this->slug = $request->getURIData('slug');
$slug = PhabricatorSlug::normalize($this->slug);
if ($slug != $this->slug) {
$uri = PhrictionDocument::getSlugURI($slug);
// Canonicalize pages to their one true URI.
return id(new AphrontRedirectResponse())->setURI($uri);
}
- require_celerity_resource('phriction-document-css');
-
$version_note = null;
$core_content = '';
$move_notice = '';
$properties = null;
$content = null;
$toc = null;
+ $is_draft = false;
+
$document = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withSlugs(array($slug))
->needContent(true)
->executeOne();
if (!$document) {
$document = PhrictionDocument::initializeNewDocument($viewer, $slug);
if ($slug == '/') {
$title = pht('Welcome to Phriction');
$subtitle = pht('Phriction is a simple and easy to use wiki for '.
'keeping track of documents and their changes.');
$page_title = pht('Welcome');
$create_text = pht('Edit this Document');
} else {
$title = pht('No Document Here');
$subtitle = pht('There is no document here, but you may create it.');
$page_title = pht('Page Not Found');
$create_text = pht('Create this Document');
}
$create_uri = '/phriction/edit/?slug='.$slug;
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText($create_text)
->setHref($create_uri)
->setColor(PHUIButtonView::GREEN);
$core_content = id(new PHUIBigInfoView())
->setIcon('fa-book')
->setTitle($title)
->setDescription($subtitle)
->addAction($create_button);
} else {
- $version = $request->getInt('v');
+ $max_version = (int)$document->getMaxVersion();
+ $version = $request->getInt('v');
if ($version) {
$content = id(new PhrictionContentQuery())
->setViewer($viewer)
->withDocumentPHIDs(array($document->getPHID()))
->withVersions(array($version))
->executeOne();
if (!$content) {
return new Aphront404Response();
}
- if ($content->getID() != $document->getContentID()) {
- $version_note = id(new PHUIInfoView())
- ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
- ->appendChild(
- pht(
- 'You are viewing an older version of this document, as it '.
- 'appeared on %s.',
- phabricator_datetime($content->getDateCreated(), $viewer)));
+ // When the "v" parameter exists, the user is in history mode so we
+ // show this header even if they're looking at the current version
+ // of the document. This keeps the next/previous links working.
+
+ $view_version = (int)$content->getVersion();
+ $published_version = (int)$document->getContent()->getVersion();
+
+ if ($view_version < $published_version) {
+ $version_note = pht(
+ 'You are viewing an older version of this document, as it '.
+ 'appeared on %s.',
+ phabricator_datetime($content->getDateCreated(), $viewer));
+ } else if ($view_version > $published_version) {
+ $is_draft = true;
+ $version_note = pht(
+ 'You are viewing an unpublished draft of this document.');
+ } else {
+ $version_note = pht(
+ 'You are viewing the current published version of this document.');
+ }
+
+ $version_note = array(
+ phutil_tag(
+ 'strong',
+ array(),
+ pht('Version %d of %d: ', $view_version, $max_version)),
+ ' ',
+ $version_note,
+ );
+
+ $version_note = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
+ ->appendChild($version_note);
+
+ $document_uri = new PhutilURI($document->getURI());
+
+ if ($view_version > 1) {
+ $previous_uri = $document_uri->alter('v', ($view_version - 1));
+ } else {
+ $previous_uri = null;
+ }
+
+ if ($view_version !== $published_version) {
+ $current_uri = $document_uri->alter('v', $published_version);
+ } else {
+ $current_uri = null;
}
+
+ if ($view_version < $max_version) {
+ $next_uri = $document_uri->alter('v', ($view_version + 1));
+ } else {
+ $next_uri = null;
+ }
+
+ if ($view_version !== $max_version) {
+ $draft_uri = $document_uri->alter('v', $max_version);
+ } else {
+ $draft_uri = null;
+ }
+
+ $button_bar = id(new PHUIButtonBarView())
+ ->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-backward')
+ ->setDisabled(!$previous_uri)
+ ->setHref($previous_uri)
+ ->setText(pht('Previous')))
+ ->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-file-o')
+ ->setDisabled(!$current_uri)
+ ->setHref($current_uri)
+ ->setText(pht('Published')))
+ ->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-forward', false)
+ ->setDisabled(!$next_uri)
+ ->setHref($next_uri)
+ ->setText(pht('Next')))
+ ->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-fast-forward', false)
+ ->setDisabled(!$draft_uri)
+ ->setHref($draft_uri)
+ ->setText(pht('Draft')));
+
+ require_celerity_resource('phui-document-view-css');
+
+ $version_note = array(
+ $version_note,
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-document-version-navigation',
+ ),
+ $button_bar),
+ );
} else {
$content = $document->getContent();
}
$page_title = $content->getTitle();
$properties = $this
->buildPropertyListView($document, $content, $slug);
$doc_status = $document->getStatus();
$current_status = $content->getChangeType();
if ($current_status == PhrictionChangeType::CHANGE_EDIT ||
$current_status == PhrictionChangeType::CHANGE_MOVE_HERE) {
$remarkup_view = $content->newRemarkupView($viewer);
$core_content = $remarkup_view->render();
$toc = $remarkup_view->getTableOfContents();
$toc = $this->getToc($toc);
} else if ($current_status == PhrictionChangeType::CHANGE_DELETE) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('Document Deleted'));
$notice->appendChild(
pht('This document has been deleted. You can edit it to put new '.
'content here, or use history to revert to an earlier version.'));
$core_content = $notice->render();
} else if ($current_status == PhrictionChangeType::CHANGE_STUB) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('Empty Document'));
$notice->appendChild(
pht('This document is empty. You can edit it to put some proper '.
'content here.'));
$core_content = $notice->render();
} else if ($current_status == PhrictionChangeType::CHANGE_MOVE_AWAY) {
$new_doc_id = $content->getChangeRef();
$slug_uri = null;
// If the new document exists and the viewer can see it, provide a link
// to it. Otherwise, render a generic message.
$new_docs = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withIDs(array($new_doc_id))
->execute();
if ($new_docs) {
$new_doc = head($new_docs);
$slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug());
}
$notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
if ($slug_uri) {
$notice->appendChild(
phutil_tag(
'p',
array(),
pht(
'This document has been moved to %s. You can edit it to put '.
'new content here, or use history to revert to an earlier '.
'version.',
phutil_tag('a', array('href' => $slug_uri), $slug_uri))));
} else {
$notice->appendChild(
phutil_tag(
'p',
array(),
pht(
'This document has been moved. You can edit it to put new '.
'content here, or use history to revert to an earlier '.
'version.')));
}
$core_content = $notice->render();
} else {
throw new Exception(pht("Unknown document status '%s'!", $doc_status));
}
$move_notice = null;
if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) {
$from_doc_id = $content->getChangeRef();
$slug_uri = null;
// If the old document exists and is visible, provide a link to it.
$from_docs = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withIDs(array($from_doc_id))
->execute();
if ($from_docs) {
$from_doc = head($from_docs);
$slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug());
}
$move_notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
if ($slug_uri) {
$move_notice->appendChild(
pht(
'This document was moved from %s.',
phutil_tag('a', array('href' => $slug_uri), $slug_uri)));
} else {
// Render this for consistency, even though it's a bit silly.
$move_notice->appendChild(
pht('This document was moved from elsewhere.'));
}
}
}
$children = $this->renderDocumentChildren($slug);
- $actions = $this->buildActionView($viewer, $document);
+ $curtain = null;
+ if ($document->getID()) {
+ $curtain = $this->buildCurtain($document, $content);
+ }
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumb_views = $this->renderBreadcrumbs($slug);
foreach ($crumb_views as $view) {
$crumbs->addCrumb($view);
}
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setPolicyObject($document)
- ->setHeader($page_title)
- ->setActionList($actions);
+ ->setHeader($page_title);
- if ($content) {
+ if ($is_draft) {
+ $draft_tag = id(new PHUITagView())
+ ->setName(pht('Draft'))
+ ->setIcon('fa-spinner')
+ ->setColor('pink')
+ ->setType(PHUITagView::TYPE_SHADE);
+
+ $header->addTag($draft_tag);
+ } else if ($content) {
$header->setEpoch($content->getDateCreated());
}
$prop_list = null;
if ($properties) {
$prop_list = new PHUIPropertyGroupView();
$prop_list->addPropertyList($properties);
}
$prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list);
- $page_content = id(new PHUIDocumentViewPro())
+ $page_content = id(new PHUIDocumentView())
+ ->setBanner($version_note)
->setHeader($header)
->setToc($toc)
->appendChild(
array(
- $version_note,
$move_notice,
$core_content,
));
+ if ($curtain) {
+ $page_content->setCurtain($curtain);
+ }
+
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($document->getPHID()))
- ->appendChild(array(
- $page_content,
- $prop_list,
- $children,
- ));
+ ->appendChild(
+ array(
+ $page_content,
+ $prop_list,
+ $children,
+ ));
}
private function buildPropertyListView(
PhrictionDocument $document,
PhrictionContent $content,
$slug) {
- $viewer = $this->getRequest()->getUser();
+ $viewer = $this->getViewer();
+
$view = id(new PHUIPropertyListView())
- ->setUser($viewer)
- ->setObject($document);
+ ->setUser($viewer);
$view->addProperty(
pht('Last Author'),
$viewer->renderHandle($content->getAuthorPHID()));
+ $view->addProperty(
+ pht('Last Edited'),
+ phabricator_datetime($content->getDateCreated(), $viewer));
+
return $view;
}
- private function buildActionView(
- PhabricatorUser $viewer,
- PhrictionDocument $document) {
+ private function buildCurtain(
+ PhrictionDocument $document,
+ PhrictionContent $content) {
+ $viewer = $this->getViewer();
+
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$document,
PhabricatorPolicyCapability::CAN_EDIT);
$slug = PhabricatorSlug::normalize($this->slug);
+ $id = $document->getID();
- $action_view = id(new PhabricatorActionListView())
- ->setUser($viewer)
- ->setObject($document);
+ $curtain = $this->newCurtainView($document);
- if (!$document->getID()) {
- return $action_view->addAction(
- id(new PhabricatorActionView())
- ->setName(pht('Create This Document'))
- ->setIcon('fa-plus-square')
- ->setHref('/phriction/edit/?slug='.$slug));
- }
-
- $action_view->addAction(
+ $curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Document'))
->setDisabled(!$can_edit)
->setIcon('fa-pencil')
->setHref('/phriction/edit/'.$document->getID().'/'));
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('View History'))
+ ->setIcon('fa-history')
+ ->setHref(PhrictionDocument::getSlugURI($slug, 'history')));
+
+ $is_current = false;
+ $content_id = null;
+ $is_draft = false;
+ if ($content) {
+ if ($content->getPHID() == $document->getContentPHID()) {
+ $is_current = true;
+ }
+ $content_id = $content->getID();
+
+ $current_version = $document->getContent()->getVersion();
+ $is_draft = ($content->getVersion() >= $current_version);
+ }
+ $can_publish = ($can_edit && $content && !$is_current);
+
+ if ($is_draft) {
+ $publish_name = pht('Publish Draft');
+ } else {
+ $publish_name = pht('Publish Revert');
+ }
+
+ $publish_uri = "/phriction/publish/{$id}/{$content_id}/";
+
+ if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
+ $publish_name = pht('Publish (Prototype!)');
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName($publish_name)
+ ->setIcon('fa-upload')
+ ->setDisabled(!$can_publish)
+ ->setWorkflow(true)
+ ->setHref($publish_uri));
+ }
+
if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) {
- $action_view->addAction(
+ $curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Move Document'))
->setDisabled(!$can_edit)
->setIcon('fa-arrows')
->setHref('/phriction/move/'.$document->getID().'/')
->setWorkflow(true));
- $action_view->addAction(
+ $curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete Document'))
->setDisabled(!$can_edit)
->setIcon('fa-times')
->setHref('/phriction/delete/'.$document->getID().'/')
->setWorkflow(true));
}
- $action_view->addAction(
- id(new PhabricatorActionView())
- ->setName(pht('View History'))
- ->setIcon('fa-list')
- ->setHref(PhrictionDocument::getSlugURI($slug, 'history')));
-
$print_uri = PhrictionDocument::getSlugURI($slug).'?__print__=1';
- $action_view->addAction(
+ $curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Printable Page'))
->setIcon('fa-print')
->setOpenInNewWindow(true)
->setHref($print_uri));
- return $action_view;
-
+ return $curtain;
}
private function renderDocumentChildren($slug) {
$d_child = PhabricatorSlug::getDepth($slug) + 1;
$d_grandchild = PhabricatorSlug::getDepth($slug) + 2;
$limit = 250;
$query = id(new PhrictionDocumentQuery())
->setViewer($this->getRequest()->getUser())
->withDepths(array($d_child, $d_grandchild))
->withSlugPrefix($slug == '/' ? '' : $slug)
->withStatuses(array(
PhrictionDocumentStatus::STATUS_EXISTS,
PhrictionDocumentStatus::STATUS_STUB,
))
->setLimit($limit)
->setOrder(PhrictionDocumentQuery::ORDER_HIERARCHY)
->needContent(true);
$children = $query->execute();
if (!$children) {
return;
}
// We're going to render in one of three modes to try to accommodate
// different information scales:
//
// - If we found fewer than $limit rows, we know we have all the children
// and grandchildren and there aren't all that many. We can just render
// everything.
// - If we found $limit rows but the results included some grandchildren,
// we just throw them out and render only the children, as we know we
// have them all.
// - If we found $limit rows and the results have no grandchildren, we
// have a ton of children. Render them and then let the user know that
// this is not an exhaustive list.
if (count($children) == $limit) {
$more_children = true;
foreach ($children as $child) {
if ($child->getDepth() == $d_grandchild) {
$more_children = false;
}
}
$show_grandchildren = false;
} else {
$show_grandchildren = true;
$more_children = false;
}
$children_dicts = array();
$grandchildren_dicts = array();
foreach ($children as $key => $child) {
$child_dict = array(
'slug' => $child->getSlug(),
'depth' => $child->getDepth(),
'title' => $child->getContent()->getTitle(),
);
if ($child->getDepth() == $d_child) {
$children_dicts[] = $child_dict;
continue;
} else {
unset($children[$key]);
if ($show_grandchildren) {
$ancestors = PhabricatorSlug::getAncestry($child->getSlug());
$grandchildren_dicts[end($ancestors)][] = $child_dict;
}
}
}
// Fill in any missing children.
$known_slugs = mpull($children, null, 'getSlug');
foreach ($grandchildren_dicts as $slug => $ignored) {
if (empty($known_slugs[$slug])) {
$children_dicts[] = array(
'slug' => $slug,
'depth' => $d_child,
'title' => PhabricatorSlug::getDefaultTitle($slug),
'empty' => true,
);
}
}
$children_dicts = isort($children_dicts, 'title');
$list = array();
foreach ($children_dicts as $child) {
$list[] = hsprintf('<li class="remarkup-list-item">');
$list[] = $this->renderChildDocumentLink($child);
$grand = idx($grandchildren_dicts, $child['slug'], array());
if ($grand) {
$list[] = hsprintf('<ul class="remarkup-list">');
foreach ($grand as $grandchild) {
$list[] = hsprintf('<li class="remarkup-list-item">');
$list[] = $this->renderChildDocumentLink($grandchild);
$list[] = hsprintf('</li>');
}
$list[] = hsprintf('</ul>');
}
$list[] = hsprintf('</li>');
}
if ($more_children) {
$list[] = phutil_tag(
'li',
array(
'class' => 'remarkup-list-item',
),
pht('More...'));
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Document Hierarchy'));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild(phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup mlt mlb',
),
phutil_tag(
'ul',
array(
'class' => 'remarkup-list',
),
$list)));
return phutil_tag_div('phui-document-view-pro-box', $box);
}
private function renderChildDocumentLink(array $info) {
$title = nonempty($info['title'], pht('(Untitled Document)'));
$item = phutil_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($info['slug']),
),
$title);
if (isset($info['empty'])) {
$item = phutil_tag('em', array(), $item);
}
return $item;
}
protected function getDocumentSlug() {
return $this->slug;
}
protected function getToc($toc) {
if ($toc) {
$toc = phutil_tag_div('phui-document-toc-content', array(
phutil_tag_div(
'phui-document-toc-header',
pht('Contents')),
$toc,
));
}
return $toc;
}
}
diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php
index e7e2b40d8..2c4718700 100644
--- a/src/applications/phriction/controller/PhrictionEditController.php
+++ b/src/applications/phriction/controller/PhrictionEditController.php
@@ -1,325 +1,339 @@
<?php
final class PhrictionEditController
extends PhrictionController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
- $current_version = null;
+ $max_version = null;
if ($id) {
$is_new = false;
$document = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withIDs(array($id))
->needContent(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$document) {
return new Aphront404Response();
}
- $current_version = $document->getContent()->getVersion();
+ $max_version = $document->getMaxVersion();
$revert = $request->getInt('revert');
if ($revert) {
$content = id(new PhrictionContentQuery())
->setViewer($viewer)
->withDocumentPHIDs(array($document->getPHID()))
->withVersions(array($revert))
->executeOne();
if (!$content) {
return new Aphront404Response();
}
} else {
- $content = $document->getContent();
+ $content = id(new PhrictionContentQuery())
+ ->setViewer($viewer)
+ ->withDocumentPHIDs(array($document->getPHID()))
+ ->setLimit(1)
+ ->executeOne();
}
-
} else {
$slug = $request->getStr('slug');
$slug = PhabricatorSlug::normalize($slug);
if (!$slug) {
return new Aphront404Response();
}
$document = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withSlugs(array($slug))
->needContent(true)
->executeOne();
if ($document) {
- $content = $document->getContent();
- $current_version = $content->getVersion();
+ $content = id(new PhrictionContentQuery())
+ ->setViewer($viewer)
+ ->withDocumentPHIDs(array($document->getPHID()))
+ ->setLimit(1)
+ ->executeOne();
+
+ $max_version = $document->getMaxVersion();
$is_new = false;
} else {
$document = PhrictionDocument::initializeNewDocument($viewer, $slug);
$content = $document->getContent();
$is_new = true;
}
}
if ($request->getBool('nodraft')) {
$draft = null;
$draft_key = null;
} else {
if ($document->getPHID()) {
$draft_key = $document->getPHID().':'.$content->getVersion();
} else {
$draft_key = 'phriction:'.$content->getSlug();
}
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$viewer->getPHID(),
$draft_key);
}
if ($draft &&
strlen($draft->getDraft()) &&
($draft->getDraft() != $content->getContent())) {
$content_text = $draft->getDraft();
$discard = phutil_tag(
'a',
array(
'href' => $request->getRequestURI()->alter('nodraft', true),
),
pht('discard this draft'));
$draft_note = new PHUIInfoView();
$draft_note->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$draft_note->setTitle(pht('Recovered Draft'));
$draft_note->appendChild(
pht('Showing a saved draft of your edits, you can %s.', $discard));
} else {
$content_text = $content->getContent();
$draft_note = null;
}
require_celerity_resource('phriction-document-css');
$e_title = true;
$e_content = true;
$validation_exception = null;
$notes = null;
$title = $content->getTitle();
$overwrite = false;
$v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$document->getPHID());
if ($is_new) {
$v_projects = array();
} else {
$v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
$document->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$v_projects = array_reverse($v_projects);
}
+ $v_space = $document->getSpacePHID();
+
if ($request->isFormPost()) {
$title = $request->getStr('title');
$content_text = $request->getStr('content');
$notes = $request->getStr('description');
- $current_version = $request->getInt('contentVersion');
+ $max_version = $request->getInt('contentVersion');
$v_view = $request->getStr('viewPolicy');
$v_edit = $request->getStr('editPolicy');
$v_cc = $request->getArr('cc');
$v_projects = $request->getArr('projects');
+ $v_space = $request->getStr('spacePHID');
$xactions = array();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue($content_text);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($v_view);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($v_edit);
+ $xactions[] = id(new PhrictionTransaction())
+ ->setTransactionType(PhabricatorTransactions::TYPE_SPACE)
+ ->setNewValue($v_space);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('=' => $v_cc));
$proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $proj_edge_type)
->setNewValue(array('=' => array_fuse($v_projects)));
$editor = id(new PhrictionTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setDescription($notes)
->setProcessContentVersionError(!$request->getBool('overwrite'))
- ->setContentVersion($current_version);
+ ->setContentVersion($max_version);
try {
$editor->applyTransactions($document, $xactions);
if ($draft) {
$draft->delete();
}
$uri = PhrictionDocument::getSlugURI($document->getSlug());
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_title = nonempty(
$ex->getShortMessage(
PhrictionDocumentTitleTransaction::TRANSACTIONTYPE),
true);
$e_content = nonempty(
$ex->getShortMessage(
PhrictionDocumentContentTransaction::TRANSACTIONTYPE),
true);
// if we're not supposed to process the content version error, then
// overwrite that content...!
if (!$editor->getProcessContentVersionError()) {
$overwrite = true;
}
$document->setViewPolicy($v_view);
$document->setEditPolicy($v_edit);
+ $document->setSpacePHID($v_space);
}
}
if ($document->getID()) {
$page_title = pht('Edit Document: %s', $content->getTitle());
if ($overwrite) {
$submit_button = pht('Overwrite Changes');
} else {
$submit_button = pht('Save Changes');
}
} else {
$submit_button = pht('Create Document');
$page_title = pht('Create Document');
}
$uri = $document->getSlug();
$uri = PhrictionDocument::getSlugURI($uri);
$uri = PhabricatorEnv::getProductionURI($uri);
$cancel_uri = PhrictionDocument::getSlugURI($document->getSlug());
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($document)
->execute();
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
$edit_capability = PhabricatorPolicyCapability::CAN_EDIT;
+
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('slug', $document->getSlug())
->addHiddenInput('nodraft', $request->getBool('nodraft'))
- ->addHiddenInput('contentVersion', $current_version)
+ ->addHiddenInput('contentVersion', $max_version)
->addHiddenInput('overwrite', $overwrite)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Title'))
->setValue($title)
->setError($e_title)
->setName('title'))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('URI'))
->setValue($uri))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Content'))
->setValue($content_text)
->setError($e_content)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setName('content')
->setID('document-textarea')
->setUser($viewer))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Tags'))
->setName('projects')
->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource()))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Subscribers'))
->setName('cc')
->setValue($v_cc)
->setUser($viewer)
->setDatasource(new PhabricatorMetaMTAMailableDatasource()))
->appendChild(
id(new AphrontFormPolicyControl())
+ ->setViewer($viewer)
->setName('viewPolicy')
+ ->setSpacePHID($v_space)
->setPolicyObject($document)
->setCapability($view_capability)
- ->setPolicies($policies)
- ->setCaption(
- $document->describeAutomaticCapability($view_capability)))
+ ->setPolicies($policies))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($document)
->setCapability($edit_capability)
- ->setPolicies($policies)
- ->setCaption(
- $document->describeAutomaticCapability($edit_capability)))
+ ->setPolicies($policies))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Edit Notes'))
->setValue($notes)
->setError(null)
->setName('description'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($page_title)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader($content->getTitle())
->setPreviewURI('/phriction/preview/'.$document->getSlug())
->setControlID('document-textarea')
->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT);
$crumbs = $this->buildApplicationCrumbs();
if ($document->getID()) {
$crumbs->addTextCrumb(
$content->getTitle(),
PhrictionDocument::getSlugURI($document->getSlug()));
$crumbs->addTextCrumb(pht('Edit'));
} else {
$crumbs->addTextCrumb(pht('Create'));
}
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
- ->setFooter(array(
- $draft_note,
- $form_box,
- $preview,
- ));
+ ->setFooter(
+ array(
+ $draft_note,
+ $form_box,
+ $preview,
+ ));
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->appendChild($view);
-
}
}
diff --git a/src/applications/phriction/controller/PhrictionHistoryController.php b/src/applications/phriction/controller/PhrictionHistoryController.php
index 432b1dfde..3023d7dcc 100644
--- a/src/applications/phriction/controller/PhrictionHistoryController.php
+++ b/src/applications/phriction/controller/PhrictionHistoryController.php
@@ -1,176 +1,189 @@
<?php
final class PhrictionHistoryController
extends PhrictionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$slug = $request->getURIData('slug');
$document = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->needContent(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$current = $document->getContent();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$history = id(new PhrictionContentQuery())
->setViewer($viewer)
->withDocumentPHIDs(array($document->getPHID()))
->executeWithCursorPager($pager);
$author_phids = mpull($history, 'getAuthorPHID');
- $handles = $this->loadViewerHandles($author_phids);
+ $handles = $viewer->loadHandles($author_phids);
+
+ $max_version = (int)$document->getMaxVersion();
+ $current_version = $document->getContent()->getVersion();
$list = new PHUIObjectItemListView();
$list->setFlush(true);
-
foreach ($history as $content) {
-
- $author = $handles[$content->getAuthorPHID()]->renderLink();
$slug_uri = PhrictionDocument::getSlugURI($document->getSlug());
$version = $content->getVersion();
- $diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/');
-
- $vs_previous = null;
- if ($content->getVersion() != 1) {
- $vs_previous = $diff_uri
- ->alter('l', $content->getVersion() - 1)
- ->alter('r', $content->getVersion());
- }
-
- $vs_head = null;
- if ($content->getID() != $document->getContentID()) {
- $vs_head = $diff_uri
- ->alter('l', $content->getVersion())
- ->alter('r', $current->getVersion());
- }
+ $base_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/');
$change_type = PhrictionChangeType::getChangeTypeLabel(
$content->getChangeType());
switch ($content->getChangeType()) {
case PhrictionChangeType::CHANGE_DELETE:
$color = 'red';
break;
case PhrictionChangeType::CHANGE_EDIT:
$color = 'lightbluetext';
break;
case PhrictionChangeType::CHANGE_MOVE_HERE:
- $color = 'yellow';
+ $color = 'yellow';
break;
case PhrictionChangeType::CHANGE_MOVE_AWAY:
- $color = 'orange';
+ $color = 'orange';
break;
case PhrictionChangeType::CHANGE_STUB:
$color = 'green';
break;
default:
- throw new Exception(pht('Unknown change type!'));
+ $color = 'indigo';
break;
}
+ $version_uri = $slug_uri.'?v='.$version;
+
$item = id(new PHUIObjectItemView())
- ->setHeader(pht('%s by %s', $change_type, $author))
- ->setStatusIcon('fa-file '.$color)
- ->addAttribute(
- phutil_tag(
- 'a',
- array(
- 'href' => $slug_uri.'?v='.$version,
- ),
- pht('Version %s', $version)))
- ->addAttribute(pht('%s %s',
- phabricator_date($content->getDateCreated(), $viewer),
- phabricator_time($content->getDateCreated(), $viewer)));
-
- if ($content->getDescription()) {
- $item->addAttribute($content->getDescription());
+ ->setHref($version_uri);
+
+ if ($version > $current_version) {
+ $icon = 'fa-spinner';
+ $color = 'pink';
+ $header = pht('Draft %d', $version);
+ } else {
+ $icon = 'fa-file-o';
+ $header = pht('Version %d', $version);
+ }
+
+ if ($version == $current_version) {
+ $item->setEffect('selected');
}
- if ($vs_previous) {
- $item->addIcon(
- 'fa-reply',
- pht('Show Change'),
- array(
- 'href' => $vs_previous,
- ));
+ $item
+ ->setHeader($header)
+ ->setStatusIcon($icon.' '.$color);
+
+ $description = $content->getDescription();
+ if (strlen($description)) {
+ $item->addAttribute($description);
+ }
+
+ $item->addIcon(
+ null,
+ phabricator_datetime($content->getDateCreated(), $viewer));
+
+ $author_phid = $content->getAuthorPHID();
+ $item->addByline($viewer->renderHandle($author_phid));
+
+ $diff_uri = null;
+ if ($version > 1) {
+ $diff_uri = $base_uri
+ ->alter('l', $version - 1)
+ ->alter('r', $version);
} else {
- $item->addIcon(
- 'fa-reply grey',
- phutil_tag('em', array(), pht('No previous change')));
+ $diff_uri = null;
}
- if ($vs_head) {
- $item->addIcon(
- 'fa-reply-all',
- pht('Show Later Changes'),
- array(
- 'href' => $vs_head,
- ));
+ if ($content->getVersion() != $max_version) {
+ $compare_uri = $base_uri
+ ->alter('l', $version)
+ ->alter('r', $max_version);
} else {
- $item->addIcon(
- 'fa-reply-all grey',
- phutil_tag('em', array(), pht('No later changes')));
+ $compare_uri = null;
}
+ $button_bar = id(new PHUIButtonBarView())
+ ->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-chevron-down')
+ ->setDisabled(!$diff_uri)
+ ->setHref($diff_uri)
+ ->setText(pht('Diff')))
+ ->addButton(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-chevron-circle-up')
+ ->setDisabled(!$compare_uri)
+ ->setHref($compare_uri)
+ ->setText(pht('Compare')));
+
+ $item->setSideColumn($button_bar);
+
$list->addItem($item);
}
$crumbs = $this->buildApplicationCrumbs();
$crumb_views = $this->renderBreadcrumbs($document->getSlug());
foreach ($crumb_views as $view) {
$crumbs->addCrumb($view);
}
$crumbs->addTextCrumb(
pht('History'),
PhrictionDocument::getSlugURI($document->getSlug(), 'history'));
$crumbs->setBorder(true);
$header = new PHUIHeaderView();
$header->setHeader(phutil_tag(
'a',
array('href' => PhrictionDocument::getSlugURI($document->getSlug())),
head($history)->getTitle()));
$header->setSubheader(pht('Document History'));
$obj_box = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
$pager = id(new PHUIBoxView())
->addClass('ml')
->appendChild($pager);
$header = id(new PHUIHeaderView())
->setHeader(pht('Document History: %s', head($history)->getTitle()))
->setHeaderIcon('fa-history');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$obj_box,
$pager,
));
$title = pht('Document History');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/phriction/controller/PhrictionPublishController.php b/src/applications/phriction/controller/PhrictionPublishController.php
new file mode 100644
index 000000000..43c90cb1f
--- /dev/null
+++ b/src/applications/phriction/controller/PhrictionPublishController.php
@@ -0,0 +1,86 @@
+<?php
+
+final class PhrictionPublishController
+ extends PhrictionController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+ $id = $request->getURIData('documentID');
+ $content_id = $request->getURIData('contentID');
+
+ $document = id(new PhrictionDocumentQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->needContent(true)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_EDIT,
+ PhabricatorPolicyCapability::CAN_VIEW,
+ ))
+ ->executeOne();
+ if (!$document) {
+ return new Aphront404Response();
+ }
+
+ $document_uri = $document->getURI();
+
+ $content = id(new PhrictionContentQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($content_id))
+ ->executeOne();
+ if (!$content) {
+ return new Aphront404Response();
+ }
+
+ if ($content->getPHID() == $document->getContentPHID()) {
+ return $this->newDialog()
+ ->setTitle(pht('Already Published'))
+ ->appendChild(
+ pht(
+ 'This version of the document is already the published '.
+ 'version.'))
+ ->addCancelButton($document_uri);
+ }
+
+ $content_uri = $document_uri.'?v='.$content->getVersion();
+
+ if ($request->isFormPost()) {
+ $xactions = array();
+
+ $xactions[] = id(new PhrictionTransaction())
+ ->setTransactionType(
+ PhrictionDocumentPublishTransaction::TRANSACTIONTYPE)
+ ->setNewValue($content->getPHID());
+
+ id(new PhrictionTransactionEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->applyTransactions($document, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($document_uri);
+ }
+
+ if ($content->getVersion() < $document->getContent()->getVersion()) {
+ $title = pht('Revert Document?');
+ $body = pht(
+ 'Revert the published version of this document to an older '.
+ 'version?');
+ $button = pht('Revert');
+ } else {
+ $title = pht('Publish Draft?');
+ $body = pht(
+ 'Update the published version of this document to this newer '.
+ 'version?');
+ $button = pht('Publish');
+ }
+
+ return $this->newDialog()
+ ->setTitle($title)
+ ->appendChild($body)
+ ->addSubmitButton($button)
+ ->addCancelButton($content_uri);
+ }
+
+}
diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php
index e6bf46c15..3c045e16b 100644
--- a/src/applications/phriction/editor/PhrictionTransactionEditor.php
+++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php
@@ -1,621 +1,559 @@
<?php
final class PhrictionTransactionEditor
extends PhabricatorApplicationTransactionEditor {
const VALIDATE_CREATE_ANCESTRY = 'create';
const VALIDATE_MOVE_ANCESTRY = 'move';
private $description;
private $oldContent;
private $newContent;
private $moveAwayDocument;
private $skipAncestorCheck;
private $contentVersion;
private $processContentVersionError = true;
private $contentDiffURI;
public function setDescription($description) {
$this->description = $description;
return $this;
}
private function getDescription() {
return $this->description;
}
private function setOldContent(PhrictionContent $content) {
$this->oldContent = $content;
return $this;
}
public function getOldContent() {
return $this->oldContent;
}
private function setNewContent(PhrictionContent $content) {
$this->newContent = $content;
return $this;
}
public function getNewContent() {
return $this->newContent;
}
public function setSkipAncestorCheck($bool) {
$this->skipAncestorCheck = $bool;
return $this;
}
public function getSkipAncestorCheck() {
return $this->skipAncestorCheck;
}
public function setContentVersion($version) {
$this->contentVersion = $version;
return $this;
}
public function getContentVersion() {
return $this->contentVersion;
}
public function setProcessContentVersionError($process) {
$this->processContentVersionError = $process;
return $this;
}
public function getProcessContentVersionError() {
return $this->processContentVersionError;
}
public function setMoveAwayDocument(PhrictionDocument $document) {
$this->moveAwayDocument = $document;
return $this;
}
public function getEditorApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
public function getEditorObjectsDescription() {
return pht('Phriction Documents');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
- protected function shouldApplyInitialEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
-
- foreach ($xactions as $xaction) {
- switch ($xaction->getTransactionType()) {
- case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE:
- return true;
- }
- }
- return parent::shouldApplyInitialEffects($object, $xactions);
- }
-
- protected function applyInitialEffects(
+ protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->setOldContent($object->getContent());
- $this->setNewContent($this->buildNewContentTemplate($object));
+
+ return parent::expandTransactions($object, $xactions);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
if ($this->getIsNewObject()) {
break;
}
$content = $xaction->getNewValue();
if ($content === '') {
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE)
->setNewValue(true)
->setMetadataValue('contentDelete', true);
}
break;
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$document = $xaction->getNewValue();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($document->getViewPolicy());
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($document->getEditPolicy());
break;
default:
break;
-
}
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
- $save_content = false;
- foreach ($xactions as $xaction) {
- switch ($xaction->getTransactionType()) {
- case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE:
- case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
- $save_content = true;
- break;
- default:
- break;
- }
- }
-
- if ($save_content) {
- $content = $this->getNewContent();
- $content->setDocumentID($object->getID());
- $content->save();
+ if ($this->hasNewDocumentContent()) {
+ $content = $this->getNewDocumentContent($object);
- $object->setContentID($content->getID());
- $object->save();
- $object->attachContent($content);
+ $content
+ ->setDocumentPHID($object->getPHID())
+ ->save();
}
if ($this->getIsNewObject() && !$this->getSkipAncestorCheck()) {
// Stub out empty parent documents if they don't exist
$ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
if ($ancestral_slugs) {
$ancestors = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs($ancestral_slugs)
->needContent(true)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
$stub_type = PhrictionChangeType::CHANGE_STUB;
foreach ($ancestral_slugs as $slug) {
$ancestor_doc = idx($ancestors, $slug);
// We check for change type to prevent near-infinite recursion
if (!$ancestor_doc && $content->getChangeType() != $stub_type) {
$ancestor_doc = PhrictionDocument::initializeNewDocument(
$this->getActor(),
$slug);
$stub_xactions = array();
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue(PhabricatorSlug::getDefaultTitle($slug))
->setMetadataValue('stub:create:phid', $object->getPHID());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue('')
->setMetadataValue('stub:create:phid', $object->getPHID());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($object->getViewPolicy());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($object->getEditPolicy());
$sub_editor = id(new PhrictionTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setSkipAncestorCheck(true)
->setDescription(pht('Empty Parent Document'))
->applyTransactions($ancestor_doc, $stub_xactions);
}
}
}
}
if ($this->moveAwayDocument !== null) {
$move_away_xactions = array();
$move_away_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE)
->setNewValue($object);
$sub_editor = id(new PhrictionTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setDescription($this->getDescription())
->applyTransactions($this->moveAwayDocument, $move_away_xactions);
}
// Compute the content diff URI for the publishing phase.
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
$uri = id(new PhutilURI('/phriction/diff/'.$object->getID().'/'))
->alter('l', $this->getOldContent()->getVersion())
->alter('r', $this->getNewContent()->getVersion());
$this->contentDiffURI = (string)$uri;
break 2;
default:
break;
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return '[Phriction]';
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
public function getMailTagsMap() {
return array(
PhrictionTransaction::MAILTAG_TITLE =>
pht("A document's title changes."),
PhrictionTransaction::MAILTAG_CONTENT =>
pht("A document's content changes."),
PhrictionTransaction::MAILTAG_DELETE =>
pht('A document is deleted.'),
PhrictionTransaction::MAILTAG_SUBSCRIBERS =>
pht('A document\'s subscribers change.'),
PhrictionTransaction::MAILTAG_OTHER =>
pht('Other document activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhrictionReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$title = $object->getContent()->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject($title);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('DOCUMENT CONTENT'),
$object->getContent()->getContent());
} else if ($this->contentDiffURI) {
$body->addLinkSection(
pht('DOCUMENT DIFF'),
PhabricatorEnv::getProductionURI($this->contentDiffURI));
}
$description = $object->getContent()->getDescription();
if (strlen($description)) {
$body->addTextSection(
pht('EDIT NOTES'),
$description);
}
$body->addLinkSection(
pht('DOCUMENT DETAIL'),
PhabricatorEnv::getProductionURI(
PhrictionDocument::getSlugURI($object->getSlug())));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = parent::getFeedRelatedPHIDs($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$dict = $xaction->getNewValue();
$phids[] = $dict['phid'];
break;
}
}
return $phids;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
foreach ($xactions as $xaction) {
switch ($type) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
if ($xaction->getMetadataValue('stub:create:phid')) {
continue;
}
if ($this->getProcessContentVersionError()) {
$error = $this->validateContentVersion($object, $type, $xaction);
if ($error) {
$this->setProcessContentVersionError(false);
$errors[] = $error;
}
}
if ($this->getIsNewObject()) {
$ancestry_errors = $this->validateAncestry(
$object,
$type,
$xaction,
self::VALIDATE_CREATE_ANCESTRY);
if ($ancestry_errors) {
$errors = array_merge($errors, $ancestry_errors);
}
}
break;
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$source_document = $xaction->getNewValue();
$ancestry_errors = $this->validateAncestry(
$object,
$type,
$xaction,
self::VALIDATE_MOVE_ANCESTRY);
if ($ancestry_errors) {
$errors = array_merge($errors, $ancestry_errors);
}
$target_document = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs(array($object->getSlug()))
->needContent(true)
->executeOne();
// Prevent overwrites and no-op moves.
$exists = PhrictionDocumentStatus::STATUS_EXISTS;
if ($target_document) {
$message = null;
if ($target_document->getSlug() == $source_document->getSlug()) {
$message = pht(
'You can not move a document to its existing location. '.
'Choose a different location to move the document to.');
} else if ($target_document->getStatus() == $exists) {
$message = pht(
'You can not move this document there, because it would '.
'overwrite an existing document which is already at that '.
'location. Move or delete the existing document first.');
}
if ($message !== null) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$message,
$xaction);
$errors[] = $error;
}
}
break;
}
}
return $errors;
}
public function validateAncestry(
PhabricatorLiskDAO $object,
$type,
PhabricatorApplicationTransaction $xaction,
$verb) {
$errors = array();
// NOTE: We use the omnipotent user for these checks because policy
// doesn't matter; existence does.
$other_doc_viewer = PhabricatorUser::getOmnipotentUser();
$ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
if ($ancestral_slugs) {
$ancestors = id(new PhrictionDocumentQuery())
->setViewer($other_doc_viewer)
->withSlugs($ancestral_slugs)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
foreach ($ancestral_slugs as $slug) {
$ancestor_doc = idx($ancestors, $slug);
if (!$ancestor_doc) {
$create_uri = '/phriction/edit/?slug='.$slug;
$create_link = phutil_tag(
'a',
array(
'href' => $create_uri,
),
$slug);
switch ($verb) {
case self::VALIDATE_MOVE_ANCESTRY:
$message = pht(
'Can not move document because the parent document with '.
'slug %s does not exist!',
$create_link);
break;
case self::VALIDATE_CREATE_ANCESTRY:
$message = pht(
'Can not create document because the parent document with '.
'slug %s does not exist!',
$create_link);
break;
}
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Missing Ancestor'),
$message,
$xaction);
$errors[] = $error;
}
}
}
return $errors;
}
private function validateContentVersion(
PhabricatorLiskDAO $object,
$type,
PhabricatorApplicationTransaction $xaction) {
$error = null;
if ($this->getContentVersion() &&
- ($object->getContent()->getVersion() != $this->getContentVersion())) {
+ ($object->getMaxVersion() != $this->getContentVersion())) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Edit Conflict'),
pht(
'Another user made changes to this document after you began '.
'editing it. Do you want to overwrite their changes? '.
'(If you choose to overwrite their changes, you should review '.
'the document edit history to see what you overwrote, and '.
'then make another edit to merge the changes if necessary.)'),
$xaction);
}
return $error;
}
- protected function requireCapabilities(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- /*
- * New objects have a special case. If a user can't see
- * x/y
- * then definitely don't let them make some
- * x/y/z
- * We need to load the direct parent to handle this case.
- */
- if ($this->getIsNewObject()) {
- $actor = $this->requireActor();
- $parent_doc = null;
- $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
- // No ancestral slugs is "/"; the first person gets to play with "/".
- if ($ancestral_slugs) {
- $parent = end($ancestral_slugs);
- $parent_doc = id(new PhrictionDocumentQuery())
- ->setViewer($actor)
- ->withSlugs(array($parent))
- ->executeOne();
- // If the $actor can't see the $parent_doc then they can't create
- // the child $object; throw a policy exception.
- if (!$parent_doc) {
- id(new PhabricatorPolicyFilter())
- ->setViewer($actor)
- ->raisePolicyExceptions(true)
- ->rejectObject(
- $object,
- $object->getEditPolicy(),
- PhabricatorPolicyCapability::CAN_EDIT);
- }
-
- // If the $actor can't edit the $parent_doc then they can't create
- // the child $object; throw a policy exception.
- if (!PhabricatorPolicyFilter::hasCapability(
- $actor,
- $parent_doc,
- PhabricatorPolicyCapability::CAN_EDIT)) {
- id(new PhabricatorPolicyFilter())
- ->setViewer($actor)
- ->raisePolicyExceptions(true)
- ->rejectObject(
- $object,
- $object->getEditPolicy(),
- PhabricatorPolicyCapability::CAN_EDIT);
- }
- }
- }
- return parent::requireCapabilities($object, $xaction);
- }
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new PhrictionDocumentHeraldAdapter())
->setDocument($object);
}
- private function buildNewContentTemplate(
- PhrictionDocument $document) {
+ private function hasNewDocumentContent() {
+ return (bool)$this->newContent;
+ }
+
+ public function getNewDocumentContent(PhrictionDocument $document) {
+ if (!$this->hasNewDocumentContent()) {
+ $content = $this->newDocumentContent($document);
+
+ // Generate a PHID now so we can populate "contentPHID" before saving
+ // the document to the database: the column is not nullable so we need
+ // a value.
+ $content_phid = $content->generatePHID();
+
+ $content->setPHID($content_phid);
+
+ $document->setContentPHID($content_phid);
+ $document->attachContent($content);
+ $document->setEditedEpoch(PhabricatorTime::getNow());
+ $document->setMaxVersion($content->getVersion());
+
+ $this->newContent = $content;
+ }
+
+ return $this->newContent;
+ }
- $new_content = id(new PhrictionContent())
+ private function newDocumentContent(PhrictionDocument $document) {
+ $content = id(new PhrictionContent())
->setSlug($document->getSlug())
- ->setAuthorPHID($this->getActor()->getPHID())
+ ->setAuthorPHID($this->getActingAsPHID())
->setChangeType(PhrictionChangeType::CHANGE_EDIT)
->setTitle($this->getOldContent()->getTitle())
->setContent($this->getOldContent()->getContent())
->setDescription('');
if (strlen($this->getDescription())) {
- $new_content->setDescription($this->getDescription());
+ $content->setDescription($this->getDescription());
}
- $new_content->setVersion($this->getOldContent()->getVersion() + 1);
+ $content->setVersion($document->getMaxVersion() + 1);
- return $new_content;
+ return $content;
}
protected function getCustomWorkerState() {
return array(
'contentDiffURI' => $this->contentDiffURI,
);
}
protected function loadCustomWorkerState(array $state) {
$this->contentDiffURI = idx($state, 'contentDiffURI');
return $this;
}
}
diff --git a/src/applications/phriction/query/PhrictionContentQuery.php b/src/applications/phriction/query/PhrictionContentQuery.php
index 053f40cf9..6efab5e1c 100644
--- a/src/applications/phriction/query/PhrictionContentQuery.php
+++ b/src/applications/phriction/query/PhrictionContentQuery.php
@@ -1,128 +1,128 @@
<?php
final class PhrictionContentQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $documentPHIDs;
private $versions;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withDocumentPHIDs(array $phids) {
$this->documentPHIDs = $phids;
return $this;
}
public function withVersions(array $versions) {
$this->versions = $versions;
return $this;
}
public function newResultObject() {
return new PhrictionContent();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'c.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'c.phid IN (%Ls)',
$this->phids);
}
if ($this->versions !== null) {
$where[] = qsprintf(
$conn,
'version IN (%Ld)',
$this->versions);
}
if ($this->documentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'd.phid IN (%Ls)',
$this->documentPHIDs);
}
return $where;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinDocumentTable()) {
$joins[] = qsprintf(
$conn,
- 'JOIN %T d ON d.id = c.documentID',
+ 'JOIN %T d ON d.phid = c.documentPHID',
id(new PhrictionDocument())->getTableName());
}
return $joins;
}
protected function willFilterPage(array $contents) {
- $document_ids = mpull($contents, 'getDocumentID');
+ $document_phids = mpull($contents, 'getDocumentPHID');
$documents = id(new PhrictionDocumentQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
- ->withIDs($document_ids)
+ ->withPHIDs($document_phids)
->execute();
- $documents = mpull($documents, null, 'getID');
+ $documents = mpull($documents, null, 'getPHID');
foreach ($contents as $key => $content) {
- $document_id = $content->getDocumentID();
+ $document_phid = $content->getDocumentPHID();
- $document = idx($documents, $document_id);
+ $document = idx($documents, $document_phid);
if (!$document) {
unset($contents[$key]);
$this->didRejectResult($content);
continue;
}
$content->attachDocument($document);
}
return $contents;
}
private function shouldJoinDocumentTable() {
if ($this->documentPHIDs !== null) {
return true;
}
return false;
}
protected function getPrimaryTableAlias() {
return 'c';
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
}
diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php
index d4d0f88f7..559235863 100644
--- a/src/applications/phriction/query/PhrictionDocumentQuery.php
+++ b/src/applications/phriction/query/PhrictionDocumentQuery.php
@@ -1,407 +1,407 @@
<?php
final class PhrictionDocumentQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $slugs;
private $depths;
private $slugPrefix;
private $slugsPrefix; // c4science custo
private $statuses;
private $parentPaths;
private $ancestorPaths;
private $needContent;
const ORDER_HIERARCHY = 'hierarchy';
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withDepths(array $depths) {
$this->depths = $depths;
return $this;
}
public function withSlugPrefix($slug_prefix) {
$this->slugPrefix = $slug_prefix;
return $this;
}
// c4science customization
public function withSlugsPrefix($slugs_prefix) {
$this->slugsPrefix = $slugs_prefix;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withParentPaths(array $paths) {
$this->parentPaths = $paths;
return $this;
}
public function withAncestorPaths(array $paths) {
$this->ancestorPaths = $paths;
return $this;
}
public function needContent($need_content) {
$this->needContent = $need_content;
return $this;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
public function newResultObject() {
return new PhrictionDocument();
}
protected function willFilterPage(array $documents) {
if ($documents) {
$ancestor_slugs = array();
foreach ($documents as $key => $document) {
$document_slug = $document->getSlug();
foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) {
$ancestor_slugs[$ancestor][] = $key;
}
}
if ($ancestor_slugs) {
$table = new PhrictionDocument();
$conn_r = $table->establishConnection('r');
$ancestors = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE slug IN (%Ls)',
$document->getTableName(),
array_keys($ancestor_slugs));
$ancestors = $table->loadAllFromArray($ancestors);
$ancestors = mpull($ancestors, null, 'getSlug');
foreach ($ancestor_slugs as $ancestor_slug => $document_keys) {
$ancestor = idx($ancestors, $ancestor_slug);
foreach ($document_keys as $document_key) {
$documents[$document_key]->attachAncestor(
$ancestor_slug,
$ancestor);
}
}
}
}
// To view a Phriction document, you must also be able to view all of the
// ancestor documents. Filter out documents which have ancestors that are
// not visible.
$document_map = array();
foreach ($documents as $document) {
$document_map[$document->getSlug()] = $document;
foreach ($document->getAncestors() as $key => $ancestor) {
if ($ancestor) {
$document_map[$key] = $ancestor;
}
}
}
$filtered_map = $this->applyPolicyFilter(
$document_map,
array(PhabricatorPolicyCapability::CAN_VIEW));
// Filter all of the documents where a parent is not visible.
foreach ($documents as $document_key => $document) {
// If the document itself is not visible, filter it.
if (!isset($filtered_map[$document->getSlug()])) {
$this->didRejectResult($documents[$document_key]);
unset($documents[$document_key]);
continue;
}
// If an ancestor exists but is not visible, filter the document.
foreach ($document->getAncestors() as $ancestor_key => $ancestor) {
if (!$ancestor) {
continue;
}
if (!isset($filtered_map[$ancestor_key])) {
$this->didRejectResult($documents[$document_key]);
unset($documents[$document_key]);
break;
}
}
}
if (!$documents) {
return $documents;
}
if ($this->needContent) {
$contents = id(new PhrictionContentQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
- ->withIDs(mpull($documents, 'getContentID'))
+ ->withPHIDs(mpull($documents, 'getContentPHID'))
->execute();
- $contents = mpull($contents, null, 'getID');
+ $contents = mpull($contents, null, 'getPHID');
foreach ($documents as $key => $document) {
- $content_id = $document->getContentID();
- if (empty($contents[$content_id])) {
+ $content_phid = $document->getContentPHID();
+ if (empty($contents[$content_phid])) {
unset($documents[$key]);
continue;
}
- $document->attachContent($contents[$content_id]);
+ $document->attachContent($contents[$content_phid]);
}
}
return $documents;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->getOrderVector()->containsKey('updated')) {
$content_dao = new PhrictionContent();
$joins[] = qsprintf(
$conn,
- 'JOIN %T c ON d.contentID = c.id',
+ 'JOIN %T c ON d.contentPHID = c.phid',
$content_dao->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'd.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'd.phid IN (%Ls)',
$this->phids);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'd.slug IN (%Ls)',
$this->slugs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'd.status IN (%Ls)',
$this->statuses);
}
if ($this->slugPrefix !== null) {
$where[] = qsprintf(
$conn,
'd.slug LIKE %>',
$this->slugPrefix);
}
// c4science customization
if ($this->slugsPrefix !== null) {
foreach($this->slugsPrefix as $prefix) {
$where[] = qsprintf(
$conn,
'd.slug LIKE %>',
$prefix);
}
}
if ($this->depths !== null) {
$where[] = qsprintf(
$conn,
'd.depth IN (%Ld)',
$this->depths);
}
if ($this->parentPaths !== null || $this->ancestorPaths !== null) {
$sets = array(
array(
'paths' => $this->parentPaths,
'parents' => true,
),
array(
'paths' => $this->ancestorPaths,
'parents' => false,
),
);
$paths = array();
foreach ($sets as $set) {
$set_paths = $set['paths'];
if ($set_paths === null) {
continue;
}
if (!$set_paths) {
throw new PhabricatorEmptyQueryException(
pht('No parent/ancestor paths specified.'));
}
$is_parents = $set['parents'];
foreach ($set_paths as $path) {
$path_normal = PhabricatorSlug::normalize($path);
if ($path !== $path_normal) {
throw new Exception(
pht(
'Document path "%s" is not a valid path. The normalized '.
'form of this path is "%s".',
$path,
$path_normal));
}
$depth = PhabricatorSlug::getDepth($path_normal);
if ($is_parents) {
$min_depth = $depth + 1;
$max_depth = $depth + 1;
} else {
$min_depth = $depth + 1;
$max_depth = null;
}
$paths[] = array(
$path_normal,
$min_depth,
$max_depth,
);
}
}
$path_clauses = array();
foreach ($paths as $path) {
$parts = array();
list($prefix, $min, $max) = $path;
// If we're getting children or ancestors of the root document, they
// aren't actually stored with the leading "/" in the database, so
// just skip this part of the clause.
if ($prefix !== '/') {
$parts[] = qsprintf(
$conn,
'd.slug LIKE %>',
$prefix);
}
if ($min !== null) {
$parts[] = qsprintf(
$conn,
'd.depth >= %d',
$min);
}
if ($max !== null) {
$parts[] = qsprintf(
$conn,
'd.depth <= %d',
$max);
}
$path_clauses[] = '('.implode(') AND (', $parts).')';
}
$where[] = '('.implode(') OR (', $path_clauses).')';
}
return $where;
}
public function getBuiltinOrders() {
return parent::getBuiltinOrders() + array(
self::ORDER_HIERARCHY => array(
- 'vector' => array('depth', 'title', 'updated'),
+ 'vector' => array('depth', 'title', 'updated', 'id'),
'name' => pht('Hierarchy'),
),
);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'depth' => array(
'table' => 'd',
'column' => 'depth',
'reverse' => true,
'type' => 'int',
),
'title' => array(
'table' => 'c',
'column' => 'title',
'reverse' => true,
'type' => 'string',
),
'updated' => array(
'table' => 'd',
- 'column' => 'contentID',
+ 'column' => 'editedEpoch',
'type' => 'int',
- 'unique' => true,
+ 'unique' => false,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$document = $this->loadCursorObject($cursor);
$map = array(
'id' => $document->getID(),
'depth' => $document->getDepth(),
- 'updated' => $document->getContentID(),
+ 'updated' => $document->getEditedEpoch(),
);
foreach ($keys as $key) {
switch ($key) {
case 'title':
$map[$key] = $document->getContent()->getTitle();
break;
}
}
return $map;
}
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
$vector = $this->getOrderVector();
if ($vector->containsKey('title')) {
$query->needContent(true);
}
}
protected function getPrimaryTableAlias() {
return 'd';
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
}
diff --git a/src/applications/phriction/query/PhrictionDocumentSearchEngine.php b/src/applications/phriction/query/PhrictionDocumentSearchEngine.php
index e3d962146..49a332268 100644
--- a/src/applications/phriction/query/PhrictionDocumentSearchEngine.php
+++ b/src/applications/phriction/query/PhrictionDocumentSearchEngine.php
@@ -1,160 +1,161 @@
<?php
final class PhrictionDocumentSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Wiki Documents');
}
public function getApplicationClassName() {
return 'PhabricatorPhrictionApplication';
}
public function newQuery() {
return id(new PhrictionDocumentQuery())
->needContent(true);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
if ($map['paths']) {
$query->withSlugs($map['paths']);
}
if ($map['parentPaths']) {
$query->withParentPaths($map['parentPaths']);
}
if ($map['ancestorPaths']) {
$query->withAncestorPaths($map['ancestorPaths']);
}
return $query;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchCheckboxesField())
->setKey('statuses')
->setLabel(pht('Status'))
->setOptions(PhrictionDocumentStatus::getStatusMap()),
id(new PhabricatorSearchStringListField())
->setKey('paths')
->setIsHidden(true)
->setLabel(pht('Paths')),
id(new PhabricatorSearchStringListField())
->setKey('parentPaths')
->setIsHidden(true)
->setLabel(pht('Parent Paths')),
id(new PhabricatorSearchStringListField())
->setKey('ancestorPaths')
->setIsHidden(true)
->setLabel(pht('Ancestor Paths')),
);
}
protected function getURI($path) {
return '/phriction/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'active' => pht('Active'),
'all' => pht('All'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
case 'active':
return $query->setParameter(
'statuses',
array(
PhrictionDocumentStatus::STATUS_EXISTS,
));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $documents,
PhabricatorSavedQuery $query) {
$phids = array();
foreach ($documents as $document) {
$content = $document->getContent();
$phids[] = $content->getAuthorPHID();
}
return $phids;
}
protected function renderResultList(
array $documents,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($documents, 'PhrictionDocument');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$list->setUser($viewer);
foreach ($documents as $document) {
$content = $document->getContent();
$slug = $document->getSlug();
$author_phid = $content->getAuthorPHID();
$slug_uri = PhrictionDocument::getSlugURI($slug);
$byline = pht(
'Edited by %s',
$handles[$author_phid]->renderLink());
$updated = phabricator_datetime(
$content->getDateCreated(),
$viewer);
$item = id(new PHUIObjectItemView())
->setHeader($content->getTitle())
+ ->setObject($document)
->setHref($slug_uri)
->addByline($byline)
->addIcon('none', $updated);
$item->addAttribute($slug_uri);
$icon = $document->getStatusIcon();
$color = $document->getStatusColor();
$label = $document->getStatusDisplayName();
$item->setStatusIcon("{$icon} {$color}", $label);
if (!$document->isActive()) {
$item->setDisabled(true);
}
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No documents found.'));
return $result;
}
}
diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php
index 515fc6b7d..5c597ab88 100644
--- a/src/applications/phriction/storage/PhrictionContent.php
+++ b/src/applications/phriction/storage/PhrictionContent.php
@@ -1,139 +1,139 @@
<?php
final class PhrictionContent
extends PhrictionDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
- protected $documentID;
+ protected $documentPHID;
protected $version;
protected $authorPHID;
protected $title;
protected $slug;
protected $content;
protected $description;
protected $changeType;
protected $changeRef;
private $document = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'version' => 'uint32',
'title' => 'sort',
'slug' => 'text128',
'content' => 'text',
'changeType' => 'uint32',
'changeRef' => 'uint32?',
'description' => 'text',
),
self::CONFIG_KEY_SCHEMA => array(
- 'documentID' => array(
- 'columns' => array('documentID', 'version'),
+ 'key_version' => array(
+ 'columns' => array('documentPHID', 'version'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'slug' => array(
'columns' => array('slug'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhrictionContentPHIDType::TYPECONST;
}
public function newRemarkupView(PhabricatorUser $viewer) {
return id(new PHUIRemarkupView($viewer, $this->getContent()))
->setContextObject($this)
->setRemarkupOption(PHUIRemarkupView::OPTION_GENERATE_TOC, true)
->setGenerateTableOfContents(true);
}
public function attachDocument(PhrictionDocument $document) {
$this->document = $document;
return $this;
}
public function getDocument() {
return $this->assertAttached($this->document);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getDocument(), PhabricatorPolicyCapability::CAN_VIEW),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('documentPHID')
->setType('phid')
->setDescription(pht('Document this content is for.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('version')
->setType('int')
->setDescription(pht('Content version.')),
);
}
public function getFieldValuesForConduit() {
return array(
'documentPHID' => $this->getDocument()->getPHID(),
'version' => (int)$this->getVersion(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhrictionContentSearchEngineAttachment())
->setAttachmentKey('content'),
);
}
}
diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php
index 729d3e4ca..b6dcd6d56 100644
--- a/src/applications/phriction/storage/PhrictionDocument.php
+++ b/src/applications/phriction/storage/PhrictionDocument.php
@@ -1,331 +1,331 @@
<?php
final class PhrictionDocument extends PhrictionDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
- PhabricatorConduitResultInterface {
+ PhabricatorConduitResultInterface,
+ PhabricatorPolicyCodexInterface,
+ PhabricatorSpacesInterface {
protected $slug;
protected $depth;
- protected $contentID;
+ protected $contentPHID;
protected $status;
- protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
+ protected $spacePHID;
+ protected $editedEpoch;
+ protected $maxVersion;
private $contentObject = self::ATTACHABLE;
private $ancestors = array();
protected 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' => 'text32',
- 'mailKey' => 'bytes20',
+ 'editedEpoch' => 'epoch',
+ 'maxVersion' => 'uint32',
),
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 function getPHIDType() {
+ return PhrictionDocumentPHIDType::TYPECONST;
}
public static function initializeNewDocument(PhabricatorUser $actor, $slug) {
- $document = new PhrictionDocument();
- $document->setSlug($slug);
+ $document = id(new self())
+ ->setSlug($slug);
- $content = new PhrictionContent();
- $content->setSlug($slug);
+ $content = id(new PhrictionContent())
+ ->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());
+ $document
+ ->setViewPolicy($parent_doc->getViewPolicy())
+ ->setEditPolicy($parent_doc->getEditPolicy())
+ ->setSpacePHID($parent_doc->getSpacePHID());
} else {
$default_view_policy = PhabricatorPolicies::getMostOpenPolicy();
- $document->setViewPolicy($default_view_policy);
- $document->setEditPolicy(PhabricatorPolicies::POLICY_USER);
+ $document
+ ->setViewPolicy($default_view_policy)
+ ->setEditPolicy(PhabricatorPolicies::POLICY_USER)
+ ->setSpacePHID($actor->getDefaultSpacePHID());
}
- return $document;
- }
+ $document->setEditedEpoch(PhabricatorTime::getNow());
+ $document->setMaxVersion(0);
- public function save() {
- if (!$this->getMailKey()) {
- $this->setMailKey(Filesystem::readRandomCharacters(20));
- }
- return parent::save();
+ return $document;
}
public static function getSlugURI($slug, $type = 'document') {
static $types = array(
'document' => '/w/',
'history' => '/phriction/history/',
);
if (empty($types[$type])) {
throw new Exception(pht("Unknown URI type '%s'!", $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;
}
public function getURI() {
return self::getSlugURI($this->getSlug());
}
/* -( Status )------------------------------------------------------------- */
public function getStatusObject() {
return PhrictionDocumentStatus::newStatusObject($this->getStatus());
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusColor() {
return $this->getStatusObject()->getColor();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function isActive() {
return $this->getStatusObject()->isActive();
}
/* -( 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.');
- }
+/* -( PhabricatorSpacesInterface )----------------------------------------- */
+
- return null;
+ public function getSpacePHID() {
+ return $this->spacePHID;
}
+
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( 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();
$contents = id(new PhrictionContentQuery())
->setViewer($engine->getViewer())
->withDocumentPHIDs(array($this->getPHID()))
->execute();
foreach ($contents as $content) {
$engine->destroyObject($content);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhrictionDocumentFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhrictionDocumentFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('path')
->setType('string')
->setDescription(pht('The path to the document.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Status information about the document.')),
);
}
public function getFieldValuesForConduit() {
$status = array(
'value' => $this->getStatus(),
'name' => $this->getStatusDisplayName(),
);
return array(
'path' => $this->getSlug(),
'status' => $status,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhrictionContentSearchEngineAttachment())
->setAttachmentKey('content'),
);
}
+
+/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
+
+
+ public function newPolicyCodex() {
+ return new PhrictionDocumentPolicyCodex();
+ }
+
+
}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php
index a05154682..759121dd6 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentContentTransaction.php
@@ -1,95 +1,94 @@
<?php
final class PhrictionDocumentContentTransaction
- extends PhrictionDocumentTransactionType {
+ extends PhrictionDocumentVersionTransaction {
const TRANSACTIONTYPE = 'content';
public function generateOldValue($object) {
if ($this->getEditor()->getIsNewObject()) {
return null;
}
return $object->getContent()->getContent();
}
public function generateNewValue($object, $value) {
return $value;
}
public function applyInternalEffects($object, $value) {
$object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS);
- }
- public function applyExternalEffects($object, $value) {
- $this->getEditor()->getNewContent()->setContent($value);
+ $content = $this->getNewDocumentContent($object);
+ $content->setContent($value);
}
public function shouldHide() {
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
}
public function getActionStrength() {
return 1.3;
}
public function getActionName() {
return pht('Edited');
}
public function getTitle() {
return pht(
'%s edited the content of this document.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s edited the content of %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function hasChangeDetailView() {
return true;
}
public function getMailDiffSectionHeader() {
return pht('CHANGES TO DOCUMENT CONTENT');
}
public function newChangeDetailView() {
$viewer = $this->getViewer();
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setViewer($viewer)
->setOldText($this->getOldValue())
->setNewText($this->getNewValue());
}
public function newRemarkupChanges() {
$changes = array();
$changes[] = $this->newRemarkupChange()
->setOldValue($this->getOldValue())
->setNewValue($this->getNewValue());
return $changes;
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$content = $object->getContent()->getContent();
if ($this->isEmptyTextTransaction($content, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Documents must have content.'));
}
return $errors;
}
}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php
index 25cc7c2b2..b1d894b96 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php
@@ -1,86 +1,85 @@
<?php
final class PhrictionDocumentDeleteTransaction
- extends PhrictionDocumentTransactionType {
+ extends PhrictionDocumentVersionTransaction {
const TRANSACTIONTYPE = 'delete';
public function generateOldValue($object) {
return null;
}
public function applyInternalEffects($object, $value) {
$object->setStatus(PhrictionDocumentStatus::STATUS_DELETED);
- }
- public function applyExternalEffects($object, $value) {
- $this->getEditor()->getNewContent()->setContent('');
- $this->getEditor()->getNewContent()->setChangeType(
- PhrictionChangeType::CHANGE_DELETE);
+ $content = $this->getNewDocumentContent($object);
+
+ $content->setContent('');
+ $content->setChangeType(PhrictionChangeType::CHANGE_DELETE);
}
public function getActionStrength() {
return 1.5;
}
public function getActionName() {
return pht('Deleted');
}
public function getTitle() {
return pht(
'%s deleted this document.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s deleted %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$e_text = null;
foreach ($xactions as $xaction) {
switch ($object->getStatus()) {
case PhrictionDocumentStatus::STATUS_DELETED:
if ($xaction->getMetadataValue('contentDelete')) {
$e_text = pht(
'This document is already deleted. You must specify '.
'content to re-create the document and make further edits.');
} else {
$e_text = pht(
'An already deleted document can not be deleted.');
}
break;
case PhrictionDocumentStatus::STATUS_MOVED:
$e_text = pht('A moved document can not be deleted.');
break;
case PhrictionDocumentStatus::STATUS_STUB:
$e_text = pht('A stub document can not be deleted.');
break;
default:
break;
}
if ($e_text !== null) {
$errors[] = $this->newInvalidError($e_text);
}
}
return $errors;
}
public function getIcon() {
return 'fa-trash-o';
}
public function getColor() {
return 'red';
}
}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php
index 3cd45f73b..09ec56dd2 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php
@@ -1,66 +1,64 @@
<?php
final class PhrictionDocumentMoveAwayTransaction
- extends PhrictionDocumentTransactionType {
+ extends PhrictionDocumentVersionTransaction {
const TRANSACTIONTYPE = 'move-away';
public function generateOldValue($object) {
return null;
}
public function generateNewValue($object, $value) {
$document = $value;
$dict = array(
'id' => $document->getID(),
'phid' => $document->getPHID(),
'content' => $document->getContent()->getContent(),
'title' => $document->getContent()->getTitle(),
);
return $dict;
}
public function applyInternalEffects($object, $value) {
$object->setStatus(PhrictionDocumentStatus::STATUS_MOVED);
- }
- public function applyExternalEffects($object, $value) {
- $dict = $value;
- $this->getEditor()->getNewContent()->setContent('');
- $this->getEditor()->getNewContent()->setChangeType(
- PhrictionChangeType::CHANGE_MOVE_AWAY);
- $this->getEditor()->getNewContent()->setChangeRef($dict['id']);
+ $content = $this->getNewDocumentContent($object);
+
+ $content->setContent('');
+ $content->setChangeType(PhrictionChangeType::CHANGE_MOVE_AWAY);
+ $content->setChangeRef($value['id']);
}
public function getActionName() {
return pht('Moved Away');
}
public function getTitle() {
$new = $this->getNewValue();
return pht(
'%s moved this document to %s',
$this->renderAuthor(),
$this->renderHandleLink($new['phid']));
}
public function getTitleForFeed() {
$new = $this->getNewValue();
return pht(
'%s moved %s to %s',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandleLink($new['phid']));
}
public function getIcon() {
return 'fa-arrows';
}
public function shouldHideForFeed() {
return true;
}
}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php
index bdf2c5d8f..180c26394 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php
@@ -1,109 +1,108 @@
<?php
final class PhrictionDocumentMoveToTransaction
- extends PhrictionDocumentTransactionType {
+ extends PhrictionDocumentVersionTransaction {
const TRANSACTIONTYPE = 'move-to';
public function generateOldValue($object) {
return null;
}
public function generateNewValue($object, $value) {
$document = $value;
+
$dict = array(
'id' => $document->getID(),
'phid' => $document->getPHID(),
'content' => $document->getContent()->getContent(),
'title' => $document->getContent()->getTitle(),
);
$editor = $this->getEditor();
$editor->setMoveAwayDocument($document);
return $dict;
}
public function applyInternalEffects($object, $value) {
$object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS);
- }
- public function applyExternalEffects($object, $value) {
- $dict = $value;
- $this->getEditor()->getNewContent()->setContent($dict['content']);
- $this->getEditor()->getNewContent()->setTitle($dict['title']);
- $this->getEditor()->getNewContent()->setChangeType(
- PhrictionChangeType::CHANGE_MOVE_HERE);
- $this->getEditor()->getNewContent()->setChangeRef($dict['id']);
+ $content = $this->getNewDocumentContent($object);
+
+ $content->setContent($value['content']);
+ $content->setTitle($value['title']);
+ $content->setChangeType(PhrictionChangeType::CHANGE_MOVE_HERE);
+ $content->setChangeRef($value['id']);
}
public function getActionStrength() {
return 1.0;
}
public function getActionName() {
return pht('Moved');
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
return pht(
'%s moved this document from %s',
$this->renderAuthor(),
$this->renderHandle($new['phid']));
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
return pht(
'%s moved %s from %s',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($new['phid']));
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$e_text = null;
foreach ($xactions as $xaction) {
$source_document = $xaction->getNewValue();
switch ($source_document->getStatus()) {
case PhrictionDocumentStatus::STATUS_DELETED:
$e_text = pht('A deleted document can not be moved.');
break;
case PhrictionDocumentStatus::STATUS_MOVED:
$e_text = pht('A moved document can not be moved again.');
break;
case PhrictionDocumentStatus::STATUS_STUB:
$e_text = pht('A stub document can not be moved.');
break;
default:
$e_text = null;
break;
}
if ($e_text !== null) {
$errors[] = $this->newInvalidError($e_text);
}
}
// TODO: Move Ancestry validation here once all types are converted.
return $errors;
}
public function getIcon() {
return 'fa-arrows';
}
public function shouldHideForFeed() {
return true;
}
}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentPublishTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentPublishTransaction.php
new file mode 100644
index 000000000..324e5c2d4
--- /dev/null
+++ b/src/applications/phriction/xaction/PhrictionDocumentPublishTransaction.php
@@ -0,0 +1,71 @@
+<?php
+
+final class PhrictionDocumentPublishTransaction
+ extends PhrictionDocumentTransactionType {
+
+ const TRANSACTIONTYPE = 'publish';
+
+ public function generateOldValue($object) {
+ return $object->getContentPHID();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setContentPHID($value);
+ }
+
+ public function getActionName() {
+ return pht('Published');
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s published a new version of this document.',
+ $this->renderAuthor());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s published a new version of %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $actor = $this->getActor();
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $content_phid = $xaction->getNewValue();
+
+ // If this isn't changing anything, skip it.
+ if ($content_phid === $object->getContentPHID()) {
+ continue;
+ }
+
+ $content = id(new PhrictionContentQuery())
+ ->setViewer($actor)
+ ->withPHIDs(array($content_phid))
+ ->executeOne();
+ if (!$content) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Unable to load Content object with PHID "%s".',
+ $content_phid),
+ $xaction);
+ continue;
+ }
+
+ if ($content->getDocumentPHID() !== $object->getPHID()) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Content object "%s" can not be published because it belongs '.
+ 'to a different document.',
+ $content_phid));
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php
index 4f1ba850a..25a77efff 100644
--- a/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php
+++ b/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php
@@ -1,97 +1,120 @@
<?php
final class PhrictionDocumentTitleTransaction
- extends PhrictionDocumentTransactionType {
+ extends PhrictionDocumentVersionTransaction {
const TRANSACTIONTYPE = 'title';
public function generateOldValue($object) {
if ($this->isNewObject()) {
return null;
}
return $this->getEditor()->getOldContent()->getTitle();
}
public function applyInternalEffects($object, $value) {
$object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS);
- }
- public function applyExternalEffects($object, $value) {
- $this->getEditor()->getNewContent()->setTitle($value);
+ $content = $this->getNewDocumentContent($object);
+
+ $content->setTitle($value);
}
public function getActionStrength() {
return 1.4;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old === null) {
if ($this->getMetadataValue('stub:create:phid')) {
return pht('Stubbed');
} else {
return pht('Created');
}
}
return pht('Retitled');
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old === null) {
if ($this->getMetadataValue('stub:create:phid')) {
return pht(
'%s stubbed out this document when creating %s.',
$this->renderAuthor(),
$this->renderHandleLink(
$this->getMetadataValue('stub:create:phid')));
} else {
return pht(
'%s created this document.',
$this->renderAuthor());
}
}
return pht(
'%s changed the title from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old === null) {
return pht(
'%s created %s.',
$this->renderAuthor(),
$this->renderObject());
}
return pht(
'%s renamed %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$title = $object->getContent()->getTitle();
if ($this->isEmptyTextTransaction($title, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Documents must have a title.'));
}
+ if ($this->isNewObject()) {
+ // No ancestral slugs is "/". No ancestry checks apply when creating the
+ // root document.
+ $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
+ if ($ancestral_slugs) {
+ // You must be able to view and edit the parent document to create a new
+ // child.
+ $parent_document = id(new PhrictionDocumentQuery())
+ ->setViewer($this->getActor())
+ ->withSlugs(array(last($ancestral_slugs)))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$parent_document) {
+ $errors[] = $this->newInvalidError(
+ pht('You can not create a document which does not have a parent.'));
+ }
+ }
+ }
+
return $errors;
}
}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentVersionTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentVersionTransaction.php
new file mode 100644
index 000000000..005e617e3
--- /dev/null
+++ b/src/applications/phriction/xaction/PhrictionDocumentVersionTransaction.php
@@ -0,0 +1,10 @@
+<?php
+
+abstract class PhrictionDocumentVersionTransaction
+ extends PhrictionDocumentTransactionType {
+
+ protected function getNewDocumentContent($object) {
+ return $this->getEditor()->getNewDocumentContent($object);
+ }
+
+}
diff --git a/src/applications/policy/codex/PhabricatorPolicyCodex.php b/src/applications/policy/codex/PhabricatorPolicyCodex.php
index 43a1cc5a1..060a798e0 100644
--- a/src/applications/policy/codex/PhabricatorPolicyCodex.php
+++ b/src/applications/policy/codex/PhabricatorPolicyCodex.php
@@ -1,106 +1,127 @@
<?php
/**
* Rendering extensions that allows an object to render custom strings,
* descriptions and explanations for the policy system to help users
* understand complex policies.
*/
abstract class PhabricatorPolicyCodex
extends Phobject {
private $viewer;
private $object;
private $policy;
private $capability;
public function getPolicyShortName() {
return null;
}
public function getPolicyIcon() {
return null;
}
public function getPolicyTagClasses() {
return array();
}
public function getPolicySpecialRuleDescriptions() {
return array();
}
+ public function getDefaultPolicy() {
+ return PhabricatorPolicyQuery::getDefaultPolicyForObject(
+ $this->viewer,
+ $this->object,
+ $this->capability);
+ }
+
+ public function compareToDefaultPolicy(PhabricatorPolicy $policy) {
+ return null;
+ }
+
+ final public function getPolicySpecialRuleForCapability($capability) {
+ foreach ($this->getPolicySpecialRuleDescriptions() as $rule) {
+ if (in_array($capability, $rule->getCapabilities())) {
+ return $rule;
+ }
+ }
+
+ return null;
+ }
+
final protected function newRule() {
return new PhabricatorPolicyCodexRuleDescription();
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setObject(PhabricatorPolicyCodexInterface $object) {
$this->object = $object;
return $this;
}
final public function getObject() {
return $this->object;
}
final public function setCapability($capability) {
$this->capability = $capability;
return $this;
}
final public function getCapability() {
return $this->capability;
}
final public function setPolicy(PhabricatorPolicy $policy) {
$this->policy = $policy;
return $this;
}
final public function getPolicy() {
return $this->policy;
}
final public static function newFromObject(
PhabricatorPolicyCodexInterface $object,
PhabricatorUser $viewer) {
if (!($object instanceof PhabricatorPolicyInterface)) {
throw new Exception(
pht(
'Object (of class "%s") implements interface "%s", but must also '.
'implement interface "%s".',
get_class($object),
'PhabricatorPolicyCodexInterface',
'PhabricatorPolicyInterface'));
}
$codex = $object->newPolicyCodex();
if (!($codex instanceof PhabricatorPolicyCodex)) {
throw new Exception(
pht(
'Object (of class "%s") implements interface "%s", but defines '.
'method "%s" incorrectly: this method must return an object of '.
'class "%s".',
get_class($object),
'PhabricatorPolicyCodexInterface',
'newPolicyCodex()',
__CLASS__));
}
$codex
->setObject($object)
->setViewer($viewer);
return $codex;
}
}
diff --git a/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php b/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php
new file mode 100644
index 000000000..9bc3c81ca
--- /dev/null
+++ b/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php
@@ -0,0 +1,9 @@
+<?php
+
+final class PhabricatorPolicyStrengthConstants
+ extends PhabricatorPolicyConstants {
+
+ const WEAKER = 'weaker';
+ const STRONGER = 'stronger';
+ const ADJUSTED = 'adjusted';
+}
diff --git a/src/applications/policy/controller/PhabricatorPolicyExplainController.php b/src/applications/policy/controller/PhabricatorPolicyExplainController.php
index 845a46115..6d4348941 100644
--- a/src/applications/policy/controller/PhabricatorPolicyExplainController.php
+++ b/src/applications/policy/controller/PhabricatorPolicyExplainController.php
@@ -1,327 +1,349 @@
<?php
final class PhabricatorPolicyExplainController
extends PhabricatorPolicyController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$phid = $request->getURIData('phid');
$capability = $request->getURIData('capability');
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$policies = PhabricatorPolicyQuery::loadPolicies(
$viewer,
$object);
$policy = idx($policies, $capability);
if (!$policy) {
return new Aphront404Response();
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
$object_name = $handle->getName();
$object_uri = nonempty($handle->getURI(), '/');
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setClass('aphront-access-dialog aphront-policy-explain-dialog')
->setTitle(pht('Policy Details: %s', $object_name))
->addCancelButton($object_uri, pht('Done'));
// c4science custo
//$space_section = $this->buildSpaceSection(
// $object,
// $policy,
// $capability);
$extended_section = $this->buildExtendedSection(
$object,
$capability);
$exceptions_section = $this->buildExceptionsSection(
$object,
$capability);
$object_section = $this->buildObjectSection(
$object,
$policy,
$capability,
$handle);
$dialog->appendChild(
array(
//$space_section, //c4science custo
$extended_section,
$exceptions_section,
$object_section,
));
return $dialog;
}
private function buildSpaceSection(
PhabricatorPolicyInterface $object,
PhabricatorPolicy $policy,
$capability) {
$viewer = $this->getViewer();
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
if (!PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
return null;
}
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$object);
$spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer);
$space = idx($spaces, $space_phid);
if (!$space) {
return null;
}
$space_policies = PhabricatorPolicyQuery::loadPolicies($viewer, $space);
$space_policy = idx($space_policies, PhabricatorPolicyCapability::CAN_VIEW);
if (!$space_policy) {
return null;
}
$doc_href = PhabricatorEnv::getDoclink('Spaces User Guide');
$capability_name = $this->getCapabilityName($capability);
$space_section = id(new PHUIPolicySectionView())
->setViewer($viewer)
->setIcon('fa-th-large bluegrey')
->setHeader(pht('Space'))
->setDocumentationLink(pht('Spaces Documentation'), $doc_href)
->appendList(
array(
array(
phutil_tag('strong', array(), pht('Space:')),
' ',
$viewer->renderHandle($space_phid)->setAsTag(true),
),
array(
phutil_tag('strong', array(), pht('%s:', $capability_name)),
' ',
$space_policy->getShortName(),
),
))
->appendParagraph(
pht(
'This object is in %s and can only be seen or edited by users '.
'with access to view objects in the space.',
$viewer->renderHandle($space_phid)));
$space_explanation = PhabricatorPolicy::getPolicyExplanation(
$viewer,
$space_policy->getPHID());
$items = array();
$items[] = $space_explanation;
$space_section
->appendParagraph(pht('Users who can see objects in this space:'))
->appendList($items);
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
if ($capability == $view_capability) {
$stronger = $space_policy->isStrongerThan($policy);
if ($stronger) {
$space_section->appendHint(
pht(
'The space this object is in has a more restrictive view '.
'policy ("%s") than the object does ("%s"), so the space\'s '.
'view policy is shown as a hint instead of the object policy.',
$space_policy->getShortName(),
$policy->getShortName()));
}
}
$space_section->appendHint(
pht(
'After a user passes space policy checks, they must still pass '.
'object policy checks.'));
return $space_section;
}
private function getStrengthInformation(
PhabricatorPolicyInterface $object,
PhabricatorPolicy $policy,
$capability) {
$viewer = $this->getViewer();
- $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject(
- $viewer,
- $object,
- $capability);
- if (!$default_policy) {
- return;
+
+ $strength = null;
+ if ($object instanceof PhabricatorPolicyCodexInterface) {
+ $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer))
+ ->setCapability($capability);
+ $strength = $codex->compareToDefaultPolicy($policy);
+ $default_policy = $codex->getDefaultPolicy();
+ } else {
+ $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject(
+ $viewer,
+ $object,
+ $capability);
+
+ if ($default_policy) {
+ if ($default_policy->getPHID() == $policy->getPHID()) {
+ return;
+ }
+
+ if ($default_policy->getPHID() != $policy->getPHID()) {
+ if ($default_policy->isStrongerThan($policy)) {
+ $strength = PhabricatorPolicyStrengthConstants::WEAKER;
+ } else if ($policy->isStrongerThan($default_policy)) {
+ $strength = PhabricatorPolicyStrengthConstants::STRONGER;
+ } else {
+ $strength = PhabricatorPolicyStrengthConstants::ADJUSTED;
+ }
+ }
+ }
}
- if ($default_policy->getPHID() == $policy->getPHID()) {
+ if (!$strength) {
return;
}
- if ($default_policy->isStrongerThan($policy)) {
+ if ($strength == PhabricatorPolicyStrengthConstants::WEAKER) {
$info = pht(
'This object has a less restrictive policy ("%s") than the default '.
'policy for similar objects (which is "%s").',
$policy->getShortName(),
$default_policy->getShortName());
- } else if ($policy->isStrongerThan($default_policy)) {
+ } else if ($strength == PhabricatorPolicyStrengthConstants::STRONGER) {
$info = pht(
'This object has a more restrictive policy ("%s") than the default '.
'policy for similar objects (which is "%s").',
$policy->getShortName(),
$default_policy->getShortName());
} else {
$info = pht(
'This object has a different policy ("%s") than the default policy '.
'for similar objects (which is "%s").',
$policy->getShortName(),
$default_policy->getShortName());
}
return $info;
}
private function getCapabilityName($capability) {
$capability_name = $capability;
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if ($capobj) {
$capability_name = $capobj->getCapabilityName();
}
return $capability_name;
}
private function buildExtendedSection(
PhabricatorPolicyInterface $object,
$capability) {
$viewer = $this->getViewer();
if (!($object instanceof PhabricatorExtendedPolicyInterface)) {
return null;
}
$extended_rules = $object->getExtendedPolicy($capability, $viewer);
if (!$extended_rules) {
return null;
}
$items = array();
foreach ($extended_rules as $extended_rule) {
$extended_target = $extended_rule[0];
$extended_capabilities = (array)$extended_rule[1];
if (is_object($extended_target)) {
$extended_target = $extended_target->getPHID();
}
foreach ($extended_capabilities as $extended_capability) {
$ex_name = $this->getCapabilityName($extended_capability);
$items[] = array(
phutil_tag('strong', array(), pht('%s:', $ex_name)),
' ',
$viewer->renderHandle($extended_target)->setAsTag(true),
);
}
}
return id(new PHUIPolicySectionView())
->setViewer($viewer)
->setIcon('fa-link')
->setHeader(pht('Required Capabilities on Other Objects'))
->appendParagraph(
pht(
'To access this object, users must have first have access '.
'capabilities on these other objects:'))
->appendList($items);
}
private function buildExceptionsSection(
PhabricatorPolicyInterface $object,
$capability) {
$viewer = $this->getViewer();
$exceptions = PhabricatorPolicy::getSpecialRules(
$object,
$viewer,
$capability,
false);
if (!$exceptions) {
return null;
}
return id(new PHUIPolicySectionView())
->setViewer($viewer)
->setIcon('fa-unlock-alt red')
->setHeader(pht('Special Rules'))
->appendParagraph(
pht(
'This object has special rules which override normal object '.
'policy rules:'))
->appendList($exceptions);
}
private function buildObjectSection(
PhabricatorPolicyInterface $object,
PhabricatorPolicy $policy,
$capability,
PhabricatorObjectHandle $handle) {
$viewer = $this->getViewer();
$capability_name = $this->getCapabilityName($capability);
$object_section = id(new PHUIPolicySectionView())
->setViewer($viewer)
->setIcon($handle->getIcon().' bluegrey')
->setHeader(pht('Object Policy'))
->appendList(
array(
array(
phutil_tag('strong', array(), pht('%s:', $capability_name)),
' ',
$policy->getShortName(),
),
))
->appendParagraph(
pht(
'In detail, this means that these users can take this action, '.
'provided they pass all of the checks described above first:'))
->appendList(
array(
PhabricatorPolicy::getPolicyExplanation(
$viewer,
$policy->getPHID()),
));
$strength = $this->getStrengthInformation($object, $policy, $capability);
if ($strength) {
$object_section->appendHint($strength);
}
return $object_section;
}
}
diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php
index 4141ef9c3..2df8fdf6a 100644
--- a/src/applications/policy/storage/PhabricatorPolicy.php
+++ b/src/applications/policy/storage/PhabricatorPolicy.php
@@ -1,512 +1,515 @@
<?php
final class PhabricatorPolicy
extends PhabricatorPolicyDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const ACTION_ALLOW = 'allow';
const ACTION_DENY = 'deny';
private $name;
private $shortName;
private $type;
private $href;
private $workflow;
private $icon;
protected $rules = array();
protected $defaultAction = self::ACTION_DENY;
private $ruleObjects = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'rules' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'defaultAction' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPolicyPHIDTypePolicy::TYPECONST);
}
public static function newFromPolicyAndHandle(
$policy_identifier,
PhabricatorObjectHandle $handle = null) {
$is_global = PhabricatorPolicyQuery::isGlobalPolicy($policy_identifier);
if ($is_global) {
return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier);
}
$policy = PhabricatorPolicyQuery::getObjectPolicy($policy_identifier);
if ($policy) {
return $policy;
}
if (!$handle) {
throw new Exception(
pht(
"Policy identifier is an object PHID ('%s'), but no object handle ".
"was provided. A handle must be provided for object policies.",
$policy_identifier));
}
$handle_phid = $handle->getPHID();
if ($policy_identifier != $handle_phid) {
throw new Exception(
pht(
"Policy identifier is an object PHID ('%s'), but the provided ".
"handle has a different PHID ('%s'). The handle must correspond ".
"to the policy identifier.",
$policy_identifier,
$handle_phid));
}
$policy = id(new PhabricatorPolicy())
->setPHID($policy_identifier)
->setHref($handle->getURI());
$phid_type = phid_get_type($policy_identifier);
switch ($phid_type) {
case PhabricatorProjectProjectPHIDType::TYPECONST:
$policy->setType(PhabricatorPolicyType::TYPE_PROJECT);
$policy->setName($handle->getName());
break;
case PhabricatorPeopleUserPHIDType::TYPECONST:
$policy->setType(PhabricatorPolicyType::TYPE_USER);
$policy->setName($handle->getFullName());
break;
case PhabricatorPolicyPHIDTypePolicy::TYPECONST:
// TODO: This creates a weird handle-based version of a rule policy.
// It behaves correctly, but can't be applied since it doesn't have
// any rules. It is used to render transactions, and might need some
// cleanup.
break;
default:
$policy->setType(PhabricatorPolicyType::TYPE_MASKED);
$policy->setName($handle->getFullName());
break;
}
$policy->makeEphemeral();
return $policy;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
if (!$this->type) {
return PhabricatorPolicyType::TYPE_CUSTOM;
}
return $this->type;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
if (!$this->name) {
return pht('Custom Policy');
}
return $this->name;
}
public function setShortName($short_name) {
$this->shortName = $short_name;
return $this;
}
public function getShortName() {
if ($this->shortName) {
return $this->shortName;
}
return $this->getName();
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function getWorkflow() {
return $this->workflow;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
if ($this->icon) {
return $this->icon;
}
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_GLOBAL:
static $map = array(
PhabricatorPolicies::POLICY_PUBLIC => 'fa-globe',
PhabricatorPolicies::POLICY_USER => 'fa-users',
PhabricatorPolicies::POLICY_ADMIN => 'fa-eye',
PhabricatorPolicies::POLICY_NOONE => 'fa-ban',
);
return idx($map, $this->getPHID(), 'fa-question-circle');
case PhabricatorPolicyType::TYPE_USER:
return 'fa-user';
case PhabricatorPolicyType::TYPE_PROJECT:
return 'fa-briefcase';
case PhabricatorPolicyType::TYPE_CUSTOM:
case PhabricatorPolicyType::TYPE_MASKED:
return 'fa-certificate';
default:
return 'fa-question-circle';
}
}
public function getSortKey() {
return sprintf(
'%02d%s',
PhabricatorPolicyType::getPolicyTypeOrder($this->getType()),
$this->getSortName());
}
private function getSortName() {
if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) {
static $map = array(
PhabricatorPolicies::POLICY_PUBLIC => 0,
PhabricatorPolicies::POLICY_USER => 1,
PhabricatorPolicies::POLICY_ADMIN => 2,
PhabricatorPolicies::POLICY_NOONE => 3,
);
return idx($map, $this->getPHID());
}
return $this->getName();
}
public static function getPolicyExplanation(
PhabricatorUser $viewer,
$policy) {
$rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy);
if ($rule) {
return $rule->getPolicyExplanation();
}
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return pht(
'This object is public and can be viewed by anyone, even if they '.
'do not have a Phabricator account.');
case PhabricatorPolicies::POLICY_USER:
return pht('Logged in users can take this action.');
case PhabricatorPolicies::POLICY_ADMIN:
return pht('Administrators can take this action.');
case PhabricatorPolicies::POLICY_NOONE:
return pht('By default, no one can take this action.');
default:
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($policy))
->executeOne();
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
return pht(
'Members of the project "%s" can take this action.',
$handle->getFullName());
} else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
return pht(
'%s can take this action.',
$handle->getFullName());
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
return pht(
'This object has a custom policy controlling who can take this '.
'action.');
} else {
return pht(
'This object has an unknown or invalid policy setting ("%s").',
$policy);
}
}
}
public function getFullName() {
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_PROJECT:
return pht('Members of Project: %s', $this->getName());
case PhabricatorPolicyType::TYPE_MASKED:
return pht('Other: %s', $this->getName());
case PhabricatorPolicyType::TYPE_USER:
return pht('Only User: %s', $this->getName());
default:
return $this->getName();
}
}
public function renderDescription($icon = false) {
$img = null;
if ($icon) {
$img = id(new PHUIIconView())
->setIcon($this->getIcon());
}
if ($this->getHref()) {
$desc = javelin_tag(
'a',
array(
'href' => $this->getHref(),
'class' => 'policy-link',
'sigil' => $this->getWorkflow() ? 'workflow' : null,
),
array(
$img,
$this->getName(),
));
} else {
if ($img) {
$desc = array($img, $this->getName());
} else {
$desc = $this->getName();
}
}
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_PROJECT:
return pht('%s (Project)', $desc);
case PhabricatorPolicyType::TYPE_CUSTOM:
return $desc;
case PhabricatorPolicyType::TYPE_MASKED:
return pht(
'%s (You do not have permission to view policy details.)',
$desc);
default:
return $desc;
}
}
/**
* Return a list of custom rule classes (concrete subclasses of
* @{class:PhabricatorPolicyRule}) this policy uses.
*
* @return list<string> List of class names.
*/
public function getCustomRuleClasses() {
$classes = array();
foreach ($this->getRules() as $rule) {
if (!is_array($rule)) {
// This rule is invalid. We'll reject it later, but don't need to
// extract anything from it for now.
continue;
}
$class = idx($rule, 'rule');
try {
if (class_exists($class)) {
$classes[$class] = $class;
}
} catch (Exception $ex) {
continue;
}
}
return array_keys($classes);
}
/**
* Return a list of all values used by a given rule class to implement this
* policy. This is used to bulk load data (like project memberships) in order
* to apply policy filters efficiently.
*
* @param string Policy rule classname.
* @return list<wild> List of values used in this policy.
*/
public function getCustomRuleValues($rule_class) {
$values = array();
foreach ($this->getRules() as $rule) {
if ($rule['rule'] == $rule_class) {
$values[] = $rule['value'];
}
}
return $values;
}
public function attachRuleObjects(array $objects) {
$this->ruleObjects = $objects;
return $this;
}
public function getRuleObjects() {
return $this->assertAttached($this->ruleObjects);
}
/**
* Return `true` if this policy is stronger (more restrictive) than some
* other policy.
*
* Because policies are complicated, determining which policies are
* "stronger" is not trivial. This method uses a very coarse working
* definition of policy strength which is cheap to compute, unambiguous,
* and intuitive in the common cases.
*
* This method returns `true` if the //class// of this policy is stronger
* than the other policy, even if the policies are (or might be) the same in
* practice. For example, "Members of Project X" is considered a stronger
* policy than "All Users", even though "Project X" might (in some rare
* cases) contain every user.
*
* Generally, the ordering here is:
*
* - Public
* - All Users
* - (Everything Else)
* - No One
*
* In the "everything else" bucket, we can't make any broad claims about
* which policy is stronger (and we especially can't make those claims
* cheaply).
*
* Even if we fully evaluated each policy, the two policies might be
* "Members of X" and "Members of Y", each of which permits access to some
* set of unique users. In this case, neither is strictly stronger than
* the other.
*
* @param PhabricatorPolicy Other policy.
* @return bool `true` if this policy is more restrictive than the other
* policy.
*/
public function isStrongerThan(PhabricatorPolicy $other) {
$this_policy = $this->getPHID();
$other_policy = $other->getPHID();
$strengths = array(
PhabricatorPolicies::POLICY_PUBLIC => -2,
PhabricatorPolicies::POLICY_USER => -1,
// (Default policies have strength 0.)
PhabricatorPolicies::POLICY_NOONE => 1,
);
$this_strength = idx($strengths, $this->getPHID(), 0);
$other_strength = idx($strengths, $other->getPHID(), 0);
return ($this_strength > $other_strength);
}
public function isValidPolicyForEdit() {
return $this->getType() !== PhabricatorPolicyType::TYPE_MASKED;
}
public static function getSpecialRules(
PhabricatorPolicyInterface $object,
PhabricatorUser $viewer,
$capability,
$active_only) {
+ $exceptions = array();
if ($object instanceof PhabricatorPolicyCodexInterface) {
- $codex = PhabricatorPolicyCodex::newFromObject($object, $viewer);
+ $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer))
+ ->setCapability($capability);
$rules = $codex->getPolicySpecialRuleDescriptions();
- $exceptions = array();
foreach ($rules as $rule) {
$is_active = $rule->getIsActive();
if ($is_active) {
$rule_capabilities = $rule->getCapabilities();
if ($rule_capabilities) {
if (!in_array($capability, $rule_capabilities)) {
$is_active = false;
}
}
}
if (!$is_active && $active_only) {
continue;
}
$description = $rule->getDescription();
if (!$is_active) {
$description = phutil_tag(
'span',
array(
'class' => 'phui-policy-section-view-inactive-rule',
),
$description);
}
$exceptions[] = $description;
}
- } else if (method_exists($object, 'describeAutomaticCapability')) {
- $exceptions = (array)$object->describeAutomaticCapability($capability);
- $exceptions = array_filter($exceptions);
- } else {
- $exceptions = array();
+ }
+
+ if (!$exceptions) {
+ if (method_exists($object, 'describeAutomaticCapability')) {
+ $exceptions = (array)$object->describeAutomaticCapability($capability);
+ $exceptions = array_filter($exceptions);
+ }
}
return $exceptions;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// NOTE: We implement policies only so we can comply with the interface.
// The actual query skips them, as enforcing policies on policies seems
// perilous and isn't currently required by the application.
return PhabricatorPolicies::POLICY_PUBLIC;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
index f34f22452..e50c83ab5 100644
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1,1597 +1,1691 @@
<?php
final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertTrue((bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertFalse((bool)$this->refreshProject($proj, $user2));
}
public function testApplicationPolicy() {
$user = $this->createUser()
->save();
$proj = $this->createProject($user);
$this->assertTrue(
PhabricatorPolicyFilter::hasCapability(
$user,
$proj,
PhabricatorPolicyCapability::CAN_VIEW));
// This object is visible so its handle should load normally.
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($proj->getPHID()))
->executeOne();
$this->assertEqual($proj->getPHID(), $handle->getPHID());
// Change the "Can Use Application" policy for Projecs to "No One". This
// should cause filtering checks to fail even when they are executed
// directly rather than via a Query.
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig(
'phabricator.application-settings',
array(
'PHID-APPS-PhabricatorProjectApplication' => array(
'policy' => array(
'view' => PhabricatorPolicies::POLICY_NOONE,
),
),
));
// Application visibility is cached because it does not normally change
// over the course of a single request. Drop the cache so the next filter
// test uses the new visibility.
PhabricatorCaches::destroyRequestCache();
$this->assertFalse(
PhabricatorPolicyFilter::hasCapability(
$user,
$proj,
PhabricatorPolicyCapability::CAN_VIEW));
// We should still be able to load a handle for the project, even if we
// can not see the application.
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($proj->getPHID()))
->executeOne();
// The handle should load...
$this->assertEqual($proj->getPHID(), $handle->getPHID());
// ...but be policy filtered.
$this->assertTrue($handle->getPolicyFiltered());
unset($env);
}
public function testIsViewerMemberOrWatcher() {
$user1 = $this->createUser()
->save();
$user2 = $this->createUser()
->save();
$user3 = $this->createUser()
->save();
$proj1 = $this->createProject($user1);
$proj1 = $this->refreshProject($proj1, $user1);
$this->joinProject($proj1, $user1);
$this->joinProject($proj1, $user3);
$this->watchProject($proj1, $user3);
$proj1 = $this->refreshProject($proj1, $user1);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, false, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, false);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user2->getPHID()));
$this->assertTrue($proj1->isUserWatcher($user3->getPHID()));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user->setAllowInlineCacheGeneration(true);
$proj = $this->createProject($user);
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertTrue($this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testAncestorMembers() {
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$parent = $this->createProject($user1);
$child = $this->createProject($user1, $parent);
$this->joinProject($child, $user1);
$this->joinProject($child, $user2);
$project = id(new PhabricatorProjectQuery())
->setViewer($user1)
->withPHIDs(array($child->getPHID()))
->needAncestorMembers(true)
->executeOne();
$members = array_fuse($project->getParentProject()->getMemberPHIDs());
ksort($members);
$expect = array_fuse(
array(
$user1->getPHID(),
$user2->getPHID(),
));
ksort($expect);
$this->assertEqual($expect, $members);
}
public function testAncestryQueries() {
$user = $this->createUser();
$user->save();
$ancestor = $this->createProject($user);
$parent = $this->createProject($user, $ancestor);
$child = $this->createProject($user, $parent);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$parent->getPHID(),
head($projects)->getPHID());
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$child->getPHID(),
head($projects)->getPHID());
$parent2 = $this->createProject($user, $ancestor);
$child2 = $this->createProject($user, $parent2);
$grandchild2 = $this->createProject($user, $child2);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(5, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(3, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(3, null)
->execute();
$this->assertEqual(1, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(
array(
$child->getPHID(),
$grandchild2->getPHID(),
))
->execute();
$this->assertEqual(2, count($projects));
}
public function testMemberMaterialization() {
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->joinProject($child, $user);
$parent_material = PhabricatorEdgeQuery::loadDestinationPHIDs(
$parent->getPHID(),
$material_type);
$this->assertEqual(
array($user->getPHID()),
$parent_material);
}
public function testMilestones() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$m1 = $this->createProject($user, $parent, true);
$m2 = $this->createProject($user, $parent, true);
$m3 = $this->createProject($user, $parent, true);
$this->assertEqual(1, $m1->getMilestoneNumber());
$this->assertEqual(2, $m2->getMilestoneNumber());
$this->assertEqual(3, $m3->getMilestoneNumber());
}
public function testMilestoneMembership() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$milestone = $this->createProject($user, $parent, true);
$this->joinProject($parent, $user);
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue($milestone->isUserMember($user->getPHID()));
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->needMembers(true)
->executeOne();
$this->assertEqual(
array($user->getPHID()),
$milestone->getMemberPHIDs());
}
public function testSameSlugAsName() {
// It should be OK to type the primary hashtag into "additional hashtags",
// even if the primary hashtag doesn't exist yet because you're creating
// or renaming the project.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
// In this first case, set the name and slugs at the same time.
$name = 'slugproject';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$this->applyTransactions($project, $user, $xactions);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name, $slugs));
// In this second case, set the name first and then the slugs separately.
$name2 = 'slugproject2';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name2);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name2));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name2, $slugs));
}
public function testDuplicateSlugs() {
// Creating a project with multiple duplicate slugs should succeed.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'duplicate';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input, $input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($input, $slugs));
}
public function testNormalizeSlugs() {
// When a user creates a project with slug "XxX360n0sc0perXxX", normalize
// it before writing it.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'NoRmAlIzE';
$expect = 'normalize';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($expect, $slugs));
// If another user tries to add the same slug in denormalized form, it
// should be caught and fail, even though the database version of the slug
// is normalized.
$project2 = $this->createProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input));
$caught = null;
try {
$this->applyTransactions($project2, $user, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$caught = $ex;
}
$this->assertTrue((bool)$caught);
}
public function testProjectMembersVisibility() {
// This is primarily testing that you can create a project and set the
// visibility or edit policy to "Project Members" immediately.
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$project = PhabricatorProject::initializeNewProject($user1);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue(
id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey());
$edge_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'=' => array($user1->getPHID() => $user1->getPHID()),
));
$this->applyTransactions($project, $user1, $xactions);
$this->assertTrue((bool)$this->refreshProject($project, $user1));
$this->assertFalse((bool)$this->refreshProject($project, $user2));
$this->leaveProject($project, $user1);
$this->assertFalse((bool)$this->refreshProject($project, $user1));
}
public function testParentProject() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->assertTrue(true);
$child = $this->refreshProject($child, $user);
$this->assertEqual(
$parent->getPHID(),
$child->getParentProject()->getPHID());
$this->assertEqual(1, (int)$child->getProjectDepth());
$this->assertFalse(
$child->isUserMember($user->getPHID()));
$this->assertFalse(
$child->getParentProject()->isUserMember($user->getPHID()));
$this->joinProject($child, $user);
$child = $this->refreshProject($child, $user);
$this->assertTrue(
$child->isUserMember($user->getPHID()));
$this->assertTrue(
$child->getParentProject()->isUserMember($user->getPHID()));
// Test that hiding a parent hides the child.
$user2 = $this->createUser();
$user2->save();
// Second user can see the project for now.
$this->assertTrue((bool)$this->refreshProject($child, $user2));
// Hide the parent.
$this->setViewPolicy($parent, $user, $user->getPHID());
// First user (who can see the parent because they are a member of
// the child) can see the project.
$this->assertTrue((bool)$this->refreshProject($child, $user));
// Second user can not, because they can't see the parent.
$this->assertFalse((bool)$this->refreshProject($child, $user2));
}
public function testSlugMaps() {
// When querying by slugs, slugs should be normalized and the mapping
// should be reported correctly.
$user = $this->createUser();
$user->save();
$name = 'queryslugproject';
$name2 = 'QUERYslugPROJECT';
$slug = 'queryslugextra';
$slug2 = 'QuErYSlUgExTrA';
$project = PhabricatorProject::initializeNewProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($slug));
$this->applyTransactions($project, $user, $xactions);
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$name => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($slug));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$slug => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name, $slug, $name2, $slug2));
$project_query->execute();
$map = $project_query->getSlugMap();
$expect = array(
$name => $project->getPHID(),
$slug => $project->getPHID(),
$name2 => $project->getPHID(),
$slug2 => $project->getPHID(),
);
$actual = ipull($map, 'projectPHID');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
$expect = array(
$name => $name,
$slug => $slug,
$name2 => $name,
$slug2 => $slug,
);
$actual = ipull($map, 'slug');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
(bool)$proj,
pht(
'Assumption that projects are default visible '.
'to any user when created.'));
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Arbitrary user not member of project.'));
// Join the project.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join works.'));
// Join the project again.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Joining an already-joined project is a no-op.'));
// Leave the project.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave works.'));
// Leave the project again.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leaving an already-left project is a no-op.'));
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
$this->joinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($ex instanceof Exception);
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with edit permission.'));
$this->leaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with join permission.'));
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave allowed without any permission.'));
}
public function testComplexConstraints() {
$user = $this->createUser();
$user->save();
$engineering = $this->createProject($user);
$engineering_scan = $this->createProject($user, $engineering);
$engineering_warp = $this->createProject($user, $engineering);
$exploration = $this->createProject($user);
$exploration_diplomacy = $this->createProject($user, $exploration);
$task_engineering = $this->newTask(
$user,
array($engineering),
pht('Engineering Only'));
$task_exploration = $this->newTask(
$user,
array($exploration),
pht('Exploration Only'));
$task_warp_explore = $this->newTask(
$user,
array($engineering_warp, $exploration),
pht('Warp to New Planet'));
$task_diplomacy_scan = $this->newTask(
$user,
array($engineering_scan, $exploration_diplomacy),
pht('Scan Diplomat'));
$task_diplomacy = $this->newTask(
$user,
array($exploration_diplomacy),
pht('Diplomatic Meeting'));
$task_warp_scan = $this->newTask(
$user,
array($engineering_scan, $engineering_warp),
pht('Scan Warp Drives'));
$this->assertQueryByProjects(
$user,
array(
$task_engineering,
$task_warp_explore,
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering),
pht('All Engineering'));
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering_scan),
pht('All Scan'));
$this->assertQueryByProjects(
$user,
array(
$task_warp_explore,
$task_diplomacy_scan,
),
array($engineering, $exploration),
pht('Engineering + Exploration'));
// This is testing that a query for "Parent" and "Parent > Child" works
// properly.
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering, $engineering_scan),
pht('Engineering + Scan'));
}
public function testTagAncestryConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_masonry = $this->createProject($user, $stonework);
$stonework_sculpting = $this->createProject($user, $stonework);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a descendant should remove the parent.
$this->addProjectTags($user, $task, array($stonework_masonry->getPHID()));
$this->assertEqual(
array(
$stonework_masonry->getPHID(),
),
$this->getTaskProjects($task));
// Adding an ancestor should remove the descendant.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding two tags in the same hierarchy which are not mutual ancestors
// should remove the ancestor but otherwise work fine.
$this->addProjectTags(
$user,
$task,
array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
));
$expect = array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
);
sort($expect);
$this->assertEqual($expect, $this->getTaskProjects($task));
}
public function testTagMilestoneConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_1 = $this->createProject($user, $stonework, true);
$stonework_2 = $this->createProject($user, $stonework, true);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a milesone should remove the parent.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
$this->assertEqual(
array(
$stonework_1->getPHID(),
),
$this->getTaskProjects($task));
// Adding the parent should remove the milestone.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// First, add one milestone.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
// Now, adding a second milestone should remove the first milestone.
$this->addProjectTags($user, $task, array($stonework_2->getPHID()));
$this->assertEqual(
array(
$stonework_2->getPHID(),
),
$this->getTaskProjects($task));
}
public function testBoardMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
$column = $this->addColumn($user, $board, 1);
// New tasks should appear in the backlog.
$task1 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Moving a task should move it to the destination column.
$this->moveToColumn($user, $board, $task1, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Same thing again, with a new task.
$task2 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Move it, too.
$this->moveToColumn($user, $board, $task2, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Now the stuff should be in the column, in order, with the more recently
// moved task on top.
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
// Move the second task after the first task.
$options = array(
'afterPHID' => $task1->getPHID(),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task1->getPHID(),
$task2->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
// Move the second task before the first task.
$options = array(
'beforePHID' => $task1->getPHID(),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$this->assertTasksInColumn($expect, $user, $board, $column);
}
public function testMilestoneMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
// Create a task into the backlog.
$task = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
$milestone = $this->createProject($user, $board, true);
$this->addProjectTags($user, $task, array($milestone->getPHID()));
// We just want the side effect of looking at the board: creation of the
// milestone column.
$this->loadColumns($user, $board, $task);
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs(array($board->getPHID()))
->withProxyPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue((bool)$column);
// Moving the task to the milestone should have moved it to the milestone
// column.
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
// Move the task within the "Milestone" column. This should not affect
// the projects the task is tagged with. See T10912.
$task_a = $task;
$task_b = $this->newTask($user, array($backlog));
$this->moveToColumn($user, $board, $task_b, $backlog, $column);
$a_options = array(
'beforePHID' => $task_b->getPHID(),
);
$b_options = array(
'beforePHID' => $task_a->getPHID(),
);
$old_projects = $this->getTaskProjects($task);
// Move the target task to the top.
$this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Move the other task.
$this->moveToColumn($user, $board, $task_b, $column, $column, $b_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Move the target task again.
$this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Add the parent project to the task. This should move it out of the
// milestone column and into the parent's backlog.
$this->addProjectTags($user, $task, array($board->getPHID()));
$expect_columns = array(
$backlog->getPHID(),
);
$this->assertColumns($expect_columns, $user, $board, $task);
$new_projects = $this->getTaskProjects($task);
$expect_projects = array(
$board->getPHID(),
);
$this->assertEqual($expect_projects, $new_projects);
}
public function testColumnExtendedPolicies() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$column = $this->addColumn($user, $board, 0);
// At first, the user should be able to view and edit the column.
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
// Now, set the project edit policy to "Members of Project". This should
// disable editing.
$members_policy = id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey();
$board->setEditPolicy($members_policy)->save();
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertFalse($can_edit);
// Now, join the project. This should make the column editable again.
$this->joinProject($board, $user);
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
// This test has been failing randomly in a way that doesn't reproduce
// on any host, so add some extra assertions to try to nail it down.
$board = $this->refreshProject($board, $user, true);
$this->assertTrue((bool)$board);
$this->assertTrue($board->isUserMember($user->getPHID()));
$can_view = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertTrue($can_view);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
}
+ public function testProjectPolicyRules() {
+ $author = $this->generateNewTestUser();
+
+ $proj_a = PhabricatorProject::initializeNewProject($author)
+ ->setName('Policy A')
+ ->save();
+ $proj_b = PhabricatorProject::initializeNewProject($author)
+ ->setName('Policy B')
+ ->save();
+
+ $user_none = $this->generateNewTestUser();
+ $user_any = $this->generateNewTestUser();
+ $user_all = $this->generateNewTestUser();
+
+ $this->joinProject($proj_a, $user_any);
+ $this->joinProject($proj_a, $user_all);
+ $this->joinProject($proj_b, $user_all);
+
+ $any_policy = id(new PhabricatorPolicy())
+ ->setRules(
+ array(
+ array(
+ 'action' => PhabricatorPolicy::ACTION_ALLOW,
+ 'rule' => 'PhabricatorProjectsPolicyRule',
+ 'value' => array(
+ $proj_a->getPHID(),
+ $proj_b->getPHID(),
+ ),
+ ),
+ ))
+ ->save();
+
+ $all_policy = id(new PhabricatorPolicy())
+ ->setRules(
+ array(
+ array(
+ 'action' => PhabricatorPolicy::ACTION_ALLOW,
+ 'rule' => 'PhabricatorProjectsAllPolicyRule',
+ 'value' => array(
+ $proj_a->getPHID(),
+ $proj_b->getPHID(),
+ ),
+ ),
+ ))
+ ->save();
+
+ $any_task = ManiphestTask::initializeNewTask($author)
+ ->setViewPolicy($any_policy->getPHID())
+ ->save();
+
+ $all_task = ManiphestTask::initializeNewTask($author)
+ ->setViewPolicy($all_policy->getPHID())
+ ->save();
+
+ $map = array(
+ array(
+ pht('Project policy rule; user in no projects'),
+ $user_none,
+ false,
+ false,
+ ),
+ array(
+ pht('Project policy rule; user in some projects'),
+ $user_any,
+ true,
+ false,
+ ),
+ array(
+ pht('Project policy rule; user in all projects'),
+ $user_all,
+ true,
+ true,
+ ),
+ );
+
+ foreach ($map as $test_case) {
+ list($label, $user, $expect_any, $expect_all) = $test_case;
+
+ $can_any = PhabricatorPolicyFilter::hasCapability(
+ $user,
+ $any_task,
+ PhabricatorPolicyCapability::CAN_VIEW);
+
+ $can_all = PhabricatorPolicyFilter::hasCapability(
+ $user,
+ $all_task,
+ PhabricatorPolicyCapability::CAN_VIEW);
+
+ $this->assertEqual($expect_any, $can_any, pht('%s / Any', $label));
+ $this->assertEqual($expect_all, $can_all, pht('%s / All', $label));
+ }
+ }
+
+
private function moveToColumn(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task,
PhabricatorProjectColumn $src,
PhabricatorProjectColumn $dst,
$options = null) {
$xactions = array();
if (!$options) {
$options = array();
}
$value = array(
'columnPHID' => $dst->getPHID(),
) + $options;
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(array($value));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function assertColumns(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$column_phids = $this->loadColumns($viewer, $board, $task);
$this->assertEqual($expect, $column_phids);
}
private function loadColumns(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs(
array(
$task->getPHID(),
))
->executeLayout();
$columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID());
$column_phids = mpull($columns, 'getPHID');
$column_phids = array_values($column_phids);
return $column_phids;
}
private function assertTasksInColumn(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
PhabricatorProjectColumn $column) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs($expect)
->executeLayout();
$object_phids = $engine->getColumnObjectPHIDs(
$board->getPHID(),
$column->getPHID());
$object_phids = array_values($object_phids);
$this->assertEqual($expect, $object_phids);
}
private function addColumn(
PhabricatorUser $viewer,
PhabricatorProject $project,
$sequence) {
$project->setHasWorkboard(1)->save();
return PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', ($sequence == 0))
->setProjectPHID($project->getPHID())
->save();
}
private function getTaskProjects(ManiphestTask $task) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
sort($project_phids);
return $project_phids;
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$params = array(
'objectIdentifier' => $proj->getID(),
'transactions' => array(
array(
'type' => 'name',
'value' => $new_name,
),
),
);
id(new ConduitCall('project.edit', $params))
->setUser($user)
->execute();
return true;
}
private function addProjectTags(
PhabricatorUser $viewer,
ManiphestTask $task,
array $phids) {
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'+' => array_fuse($phids),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function newTask(
PhabricatorUser $viewer,
array $projects,
$name = null) {
$task = ManiphestTask::initializeNewTask($viewer);
if (!strlen($name)) {
$name = pht('Test Task');
}
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setNewValue($name);
if ($projects) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse(mpull($projects, 'getPHID')),
));
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
private function assertQueryByProjects(
PhabricatorUser $viewer,
array $expect,
array $projects,
$label = null) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$project_phids = mpull($projects, 'getPHID');
$constraints = $datasource->evaluateTokens($project_phids);
$query = id(new ManiphestTaskQuery())
->setViewer($viewer);
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
$tasks = $query->execute();
$expect_phids = mpull($expect, 'getTitle', 'getPHID');
ksort($expect_phids);
$actual_phids = mpull($tasks, 'getTitle', 'getPHID');
ksort($actual_phids);
$this->assertEqual($expect_phids, $actual_phids, $label);
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false,
$need_watchers = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->needWatchers($need_watchers)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function refreshColumn(
PhabricatorUser $viewer,
PhabricatorProjectColumn $column) {
$results = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($column->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject(
PhabricatorUser $user,
PhabricatorProject $parent = null,
$is_milestone = false) {
$project = PhabricatorProject::initializeNewProject($user);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
if ($parent) {
if ($is_milestone) {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE)
->setNewValue($parent->getPHID());
} else {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectParentTransaction::TRANSACTIONTYPE)
->setNewValue($parent->getPHID());
}
}
$this->applyTransactions($project, $user, $xactions);
// Force these values immediately; they are normally updated by the
// index engine.
if ($parent) {
if ($is_milestone) {
$parent->setHasMilestones(1)->save();
} else {
$parent->setHasSubprojects(1)->save();
}
}
return $project;
}
private function setViewPolicy(
PhabricatorProject $project,
PhabricatorUser $user,
$policy) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($policy);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject($author);
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName(pht('Unit Test User %d', $rand));
return $user;
}
private function joinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '+');
}
private function leaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '-');
}
private function watchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '+');
}
private function unwatchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '-');
}
private function joinOrLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
private function watchOrUnwatchProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
private function applyProjectEdgeTransaction(
PhabricatorProject $project,
PhabricatorUser $user,
$operation,
$edge_type) {
$spec = array(
$operation => array($user->getPHID() => $user->getPHID()),
);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue($spec);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function applyTransactions(
PhabricatorProject $project,
PhabricatorUser $user,
array $xactions) {
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php
index 269e80a08..36736c78b 100644
--- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php
+++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php
@@ -1,227 +1,226 @@
<?php
final class PhabricatorProjectSubprojectsController
extends PhabricatorProjectController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$response = $this->loadProject();
if ($response) {
return $response;
}
$project = $this->getProject();
$id = $project->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$allows_subprojects = $project->supportsSubprojects();
$allows_milestones = $project->supportsMilestones();
$subproject_list = null;
$milestone_list = null;
if ($allows_subprojects) {
$subprojects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
->needImages(true)
->withIsMilestone(false)
->execute();
$subproject_list = id(new PHUIObjectBoxView())
->setHeaderText(pht('%s Subprojects', $project->getName()))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList(
id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($subprojects)
->setNoDataString(pht('This project has no subprojects.'))
->renderList());
} else {
$subprojects = array();
}
if ($allows_milestones) {
$milestones = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
->needImages(true)
->withIsMilestone(true)
->setOrderVector(array('milestoneNumber', 'id'))
->execute();
$milestone_list = id(new PHUIObjectBoxView())
->setHeaderText(pht('%s Milestones', $project->getName()))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList(
id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($milestones)
->setNoDataString(pht('This project has no milestones.'))
->renderList());
} else {
$milestones = array();
}
$curtain = $this->buildCurtainView(
$project,
$milestones,
$subprojects);
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorProject::ITEM_SUBPROJECTS);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Subprojects'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Subprojects and Milestones'))
->setHeaderIcon('fa-sitemap');
require_celerity_resource('project-view-css');
// This page isn't reachable via UI, but make it pretty anyways.
$info_view = null;
if (!$milestone_list && !$subproject_list) {
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('Milestone projects do not support subprojects '.
'or milestones.'));
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setMainColumn(array(
$info_view,
- $milestone_list,
$subproject_list,
+ $milestone_list,
));
return $this->newPage()
->setNavigation($nav)
->setCrumbs($crumbs)
->setTitle(array($project->getName(), pht('Subprojects')))
->appendChild($view);
}
private function buildCurtainView(
PhabricatorProject $project,
array $milestones,
array $subprojects) {
$viewer = $this->getViewer();
$id = $project->getID();
$can_create = $this->hasApplicationCapability(
ProjectCreateProjectsCapability::CAPABILITY);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
- $allows_milestones = $project->supportsMilestones();
$allows_subprojects = $project->supportsSubprojects();
+ $allows_milestones = $project->supportsMilestones();
$curtain = $this->newCurtainView();
- if ($allows_milestones && $milestones) {
- $milestone_text = pht('Create Next Milestone');
- } else {
- $milestone_text = pht('Create Milestone');
- }
-
- $can_milestone = ($can_create && $can_edit && $allows_milestones);
- $milestone_href = "/project/edit/?milestone={$id}";
-
- $curtain->addAction(
- id(new PhabricatorActionView())
- ->setName($milestone_text)
- ->setIcon('fa-plus')
- ->setHref($milestone_href)
- ->setDisabled(!$can_milestone)
- ->setWorkflow(!$can_milestone));
-
$can_subproject = ($can_create && $can_edit && $allows_subprojects);
// If we're offering to create the first subproject, we're going to warn
// the user about the effects before moving forward.
if ($can_subproject && !$subprojects) {
$subproject_href = "/project/warning/{$id}/";
$subproject_disabled = false;
$subproject_workflow = true;
} else {
$subproject_href = "/project/edit/?parent={$id}";
$subproject_disabled = !$can_subproject;
$subproject_workflow = !$can_subproject;
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Subproject'))
->setIcon('fa-plus')
->setHref($subproject_href)
->setDisabled($subproject_disabled)
->setWorkflow($subproject_workflow));
-
- if (!$project->supportsMilestones()) {
- $note = pht(
- 'This project is already a milestone, and milestones may not '.
- 'have their own milestones.');
+ if ($allows_milestones && $milestones) {
+ $milestone_text = pht('Create Next Milestone');
} else {
- if (!$milestones) {
- $note = pht('Milestones can be created for this project.');
- } else {
- $note = pht('This project has milestones.');
- }
+ $milestone_text = pht('Create Milestone');
}
- $curtain->newPanel()
- ->setHeaderText(pht('Milestones'))
- ->appendChild($note);
+ $can_milestone = ($can_create && $can_edit && $allows_milestones);
+ $milestone_href = "/project/edit/?milestone={$id}";
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName($milestone_text)
+ ->setIcon('fa-plus')
+ ->setHref($milestone_href)
+ ->setDisabled(!$can_milestone)
+ ->setWorkflow(!$can_milestone));
if (!$project->supportsSubprojects()) {
$note = pht(
'This project is a milestone, and milestones may not have '.
'subprojects.');
} else {
if (!$subprojects) {
$note = pht('Subprojects can be created for this project.');
} else {
$note = pht('This project has subprojects.');
}
}
$curtain->newPanel()
->setHeaderText(pht('Subprojects'))
->appendChild($note);
+ if (!$project->supportsSubprojects()) {
+ $note = pht(
+ 'This project is already a milestone, and milestones may not '.
+ 'have their own milestones.');
+ } else {
+ if (!$milestones) {
+ $note = pht('Milestones can be created for this project.');
+ } else {
+ $note = pht('This project has milestones.');
+ }
+ }
+
+ $curtain->newPanel()
+ ->setHeaderText(pht('Milestones'))
+ ->appendChild($note);
+
return $curtain;
}
private function renderStatus($icon, $target, $note) {
$item = id(new PHUIStatusItemView())
->setIcon($icon)
->setTarget(phutil_tag('strong', array(), $target))
->setNote($note);
return id(new PHUIStatusListView())
->addItem($item);
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
index 19f059b8b..d49476708 100644
--- a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
@@ -1,158 +1,140 @@
<?php
final class PhabricatorProjectColumnTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Workboard Columns');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorProjectColumnTransaction::TYPE_NAME;
$types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS;
$types[] = PhabricatorProjectColumnTransaction::TYPE_LIMIT;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectColumnTransaction::TYPE_NAME:
return $object->getName();
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
return $object->getStatus();
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
return $object->getPointLimit();
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectColumnTransaction::TYPE_NAME:
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
return $xaction->getNewValue();
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
$value = $xaction->getNewValue();
if (strlen($value)) {
return (int)$xaction->getNewValue();
} else {
return null;
}
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectColumnTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
return;
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
$object->setPointLimit($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectColumnTransaction::TYPE_NAME:
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (strlen($value) && !preg_match('/^\d+\z/', $value)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Column point limit must either be empty or a nonnegative '.
'integer.'),
$xaction);
}
}
break;
case PhabricatorProjectColumnTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
// The default "Backlog" column is allowed to be unnamed, which
// means we use the default name.
if ($missing && !$object->isDefaultColumn()) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Column name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
}
return $errors;
}
-
- protected function requireCapabilities(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorProjectColumnTransaction::TYPE_NAME:
- case PhabricatorProjectColumnTransaction::TYPE_STATUS:
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- return;
- }
-
- return parent::requireCapabilities($object, $xaction);
- }
-
}
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 2a90d6ce2..581ec2315 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,477 +1,419 @@
<?php
final class PhabricatorProjectTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $isMilestone;
private function setIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function getIsMilestone() {
return $this->isMilestone;
}
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Projects');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this project.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
return $types;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
// Prevent creating projects which are both subprojects and milestones,
// since this does not make sense, won't work, and will break everything.
$parent_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectParentTransaction::TRANSACTIONTYPE:
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
if ($xaction->getNewValue() === null) {
continue;
}
if (!$parent_xaction) {
$parent_xaction = $xaction;
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'When creating a project, specify a maximum of one parent '.
'project or milestone project. A project can not be both a '.
'subproject and a milestone.'),
$xaction);
break;
break;
}
}
$is_milestone = $this->getIsMilestone();
$is_parent = $object->getHasSubprojects();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$type = $xaction->getMetadataValue('edge:type');
if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) {
break;
}
if ($is_parent) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a project with subprojects '.
'directly. Members of any subproject are automatically '.
'members of the parent project.'),
$xaction);
}
if ($is_milestone) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a milestone. Members of the '.
'parent project are automatically members of the milestone.'),
$xaction);
}
break;
}
}
return $errors;
}
- protected function requireCapabilities(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorProjectLockTransaction::TRANSACTIONTYPE:
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- newv($this->getEditorApplicationClass(), array()),
- ProjectCanLockProjectsCapability::CAPABILITY);
- return;
- case PhabricatorTransactions::TYPE_EDGE:
- switch ($xaction->getMetadataValue('edge:type')) {
- case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
- $old = $xaction->getOldValue();
- $new = $xaction->getNewValue();
-
- $add = array_keys(array_diff_key($new, $old));
- $rem = array_keys(array_diff_key($old, $new));
-
- $actor_phid = $this->requireActor()->getPHID();
-
- $is_join = (($add === array($actor_phid)) && !$rem);
- $is_leave = (($rem === array($actor_phid)) && !$add);
-
- if ($is_join) {
- // You need CAN_JOIN to join a project.
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_JOIN);
- } else if ($is_leave) {
- // You usually don't need any capabilities to leave a project.
- if ($object->getIsMembershipLocked()) {
- // you must be able to edit though to leave locked projects
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- }
- } else {
- if (!$this->getIsNewObject()) {
- // You need CAN_EDIT to change members other than yourself.
- // (PHI193) Just skip this check if we're creating a project.
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- }
- }
- return;
- }
- break;
- }
-
- return parent::requireCapabilities($object, $xaction);
- }
-
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// NOTE: We're using the omnipotent user here because the original actor
// may no longer have permission to view the object.
return id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Project]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
protected function getMailCc(PhabricatorLiskDAO $object) {
return array();
}
public function getMailTagsMap() {
return array(
PhabricatorProjectTransaction::MAILTAG_METADATA =>
pht('Project name, hashtags, icon, image, or color changes.'),
PhabricatorProjectTransaction::MAILTAG_MEMBERS =>
pht('Project membership changes.'),
PhabricatorProjectTransaction::MAILTAG_WATCHERS =>
pht('Project watcher list changes.'),
PhabricatorProjectTransaction::MAILTAG_OTHER =>
pht('Other project activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ProjectReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$uri = '/project/profile/'.$object->getID().'/';
$body->addLinkSection(
pht('PROJECT DETAIL'),
PhabricatorEnv::getProductionURI($uri));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$materialize = false;
$new_parent = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$materialize = true;
break;
}
break;
case PhabricatorProjectParentTransaction::TRANSACTIONTYPE:
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
$materialize = true;
$new_parent = $object->getParentProject();
break;
}
}
if ($new_parent) {
// If we just created the first subproject of this parent, we want to
// copy all of the real members to the subproject.
if (!$new_parent->getHasSubprojects()) {
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$project_members = PhabricatorEdgeQuery::loadDestinationPHIDs(
$new_parent->getPHID(),
$member_type);
if ($project_members) {
$editor = id(new PhabricatorEdgeEditor());
foreach ($project_members as $phid) {
$editor->addEdge($object->getPHID(), $member_type, $phid);
}
$editor->save();
}
}
}
// TODO: We should dump an informational transaction onto the parent
// project to show that we created the sub-thing.
if ($materialize) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($object);
}
if ($new_parent) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($new_parent);
}
return parent::applyFinalEffects($object, $xactions);
}
public function addSlug(PhabricatorProject $project, $slug, $force) {
$slug = PhabricatorSlug::normalizeProjectSlug($slug);
$table = new PhabricatorProjectSlug();
$project_phid = $project->getPHID();
if ($force) {
// If we have the `$force` flag set, we only want to ignore an existing
// slug if it's for the same project. We'll error on collisions with
// other projects.
$current = $table->loadOneWhere(
'slug = %s AND projectPHID = %s',
$slug,
$project_phid);
} else {
// Without the `$force` flag, we'll just return without doing anything
// if any other project already has the slug.
$current = $table->loadOneWhere(
'slug = %s',
$slug);
}
if ($current) {
return;
}
return id(new PhabricatorProjectSlug())
->setSlug($slug)
->setProjectPHID($project_phid)
->save();
}
public function removeSlugs(PhabricatorProject $project, array $slugs) {
if (!$slugs) {
return;
}
// We're going to try to delete both the literal and normalized versions
// of all slugs. This allows us to destroy old slugs that are no longer
// valid.
foreach ($this->normalizeSlugs($slugs) as $slug) {
$slugs[] = $slug;
}
$objects = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID = %s AND slug IN (%Ls)',
$project->getPHID(),
$slugs);
foreach ($objects as $object) {
$object->delete();
}
}
public function normalizeSlugs(array $slugs) {
foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug);
}
$slugs = array_unique($slugs);
$slugs = array_values($slugs);
return $slugs;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$member_xaction = null;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() !== $type_edge) {
continue;
}
$edgetype = $xaction->getMetadataValue('edge:type');
if ($edgetype !== $edgetype_member) {
continue;
}
$member_xaction = $xaction;
}
if ($member_xaction) {
$object_phid = $object->getPHID();
if ($object_phid) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getActor())
->withPHIDs(array($object_phid))
->needMembers(true)
->executeOne();
$members = $project->getMemberPHIDs();
} else {
$members = array();
}
$clone_xaction = clone $member_xaction;
$hint = $this->getPHIDTransactionNewValue($clone_xaction, $members);
$rule = new PhabricatorProjectMembersPolicyRule();
$hint = array_fuse($hint);
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
$rule,
$hint);
}
return $copy;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_milestone = $object->isMilestone();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
if ($xaction->getNewValue() !== null) {
$is_milestone = true;
}
break;
}
}
$this->setIsMilestone($is_milestone);
return $results;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
// Herald rules may run on behalf of other users and need to execute
// membership checks against ancestors.
$project = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
return id(new PhabricatorProjectHeraldAdapter())
->setProject($project);
}
}
diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php
index a7702c345..5eb6b49b8 100644
--- a/src/applications/project/engine/PhabricatorProjectEditEngine.php
+++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php
@@ -1,321 +1,326 @@
<?php
final class PhabricatorProjectEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'projects.project';
private $parentProject;
private $milestoneProject;
public function setParentProject(PhabricatorProject $parent_project) {
$this->parentProject = $parent_project;
return $this;
}
public function getParentProject() {
return $this->parentProject;
}
public function setMilestoneProject(PhabricatorProject $milestone_project) {
$this->milestoneProject = $milestone_project;
return $this;
}
public function getMilestoneProject() {
return $this->milestoneProject;
}
public function isDefaultQuickCreateEngine() {
return true;
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())->addInt(200);
}
public function getEngineName() {
return pht('Projects');
}
public function getSummaryHeader() {
return pht('Configure Project Forms');
}
public function getSummaryText() {
return pht('Configure forms for creating projects.');
}
public function getEngineApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function newEditableObject() {
- return PhabricatorProject::initializeNewProject($this->getViewer());
+ $parent = nonempty($this->parentProject, $this->milestoneProject);
+
+ return PhabricatorProject::initializeNewProject(
+ $this->getViewer(),
+ $parent);
}
protected function newObjectQuery() {
return id(new PhabricatorProjectQuery())
->needSlugs(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Project');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Project: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return $object->getName();
}
protected function getObjectCreateShortText() {
return pht('Create Project');
}
protected function getObjectName() {
return pht('Project');
}
protected function getObjectViewURI($object) {
if ($this->getIsCreate()) {
return $object->getURI();
} else {
$id = $object->getID();
return "/project/manage/{$id}/";
}
}
protected function getObjectCreateCancelURI($object) {
$parent = $this->getParentProject();
$milestone = $this->getMilestoneProject();
if ($parent || $milestone) {
$id = nonempty($parent, $milestone)->getID();
return "/project/subprojects/{$id}/";
}
return parent::getObjectCreateCancelURI($object);
}
protected function getCreateNewObjectPolicy() {
return $this->getApplication()->getPolicy(
ProjectCreateProjectsCapability::CAPABILITY);
}
protected function willConfigureFields($object, array $fields) {
$is_milestone = ($this->getMilestoneProject() || $object->isMilestone());
$unavailable = array(
PhabricatorTransactions::TYPE_VIEW_POLICY,
PhabricatorTransactions::TYPE_EDIT_POLICY,
PhabricatorTransactions::TYPE_JOIN_POLICY,
+ PhabricatorTransactions::TYPE_SPACE,
PhabricatorProjectIconTransaction::TRANSACTIONTYPE,
PhabricatorProjectColorTransaction::TRANSACTIONTYPE,
);
$unavailable = array_fuse($unavailable);
if ($is_milestone) {
foreach ($fields as $key => $field) {
$xaction_type = $field->getTransactionType();
if (isset($unavailable[$xaction_type])) {
unset($fields[$key]);
}
}
}
return $fields;
}
protected function newBuiltinEngineConfigurations() {
$configuration = head(parent::newBuiltinEngineConfigurations());
// TODO: This whole method is clumsy, and the ordering for the custom
// field is especially clumsy. Maybe try to make this more natural to
// express.
$configuration
->setFieldOrder(
array(
'parent',
'milestone',
'milestone.previous',
'name',
'std:project:internal:description',
'icon',
'color',
'slugs',
));
return array(
$configuration,
);
}
protected function buildCustomEditFields($object) {
$slugs = mpull($object->getSlugs(), 'getSlug');
$slugs = array_fuse($slugs);
unset($slugs[$object->getPrimarySlug()]);
$slugs = array_values($slugs);
$milestone = $this->getMilestoneProject();
$parent = $this->getParentProject();
if ($parent) {
$parent_phid = $parent->getPHID();
} else {
$parent_phid = null;
}
$previous_milestone_phid = null;
if ($milestone) {
$milestone_phid = $milestone->getPHID();
// Load the current milestone so we can show the user a hint about what
// it was called, so they don't have to remember if the next one should
// be "Sprint 287" or "Sprint 278".
$number = ($milestone->loadNextMilestoneNumber() - 1);
if ($number > 0) {
$previous_milestone = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withParentProjectPHIDs(array($milestone->getPHID()))
->withIsMilestone(true)
->withMilestoneNumberBetween($number, $number)
->executeOne();
if ($previous_milestone) {
$previous_milestone_phid = $previous_milestone->getPHID();
}
}
} else {
$milestone_phid = null;
}
$fields = array(
id(new PhabricatorHandlesEditField())
->setKey('parent')
->setLabel(pht('Parent'))
->setDescription(pht('Create a subproject of an existing project.'))
->setConduitDescription(
pht('Choose a parent project to create a subproject beneath.'))
->setConduitTypeDescription(pht('PHID of the parent project.'))
->setAliases(array('parentPHID'))
->setTransactionType(
PhabricatorProjectParentTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new AphrontPHIDHTTPParameterType())
->setSingleValue($parent_phid)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setIsLocked(true),
id(new PhabricatorHandlesEditField())
->setKey('milestone')
->setLabel(pht('Milestone Of'))
->setDescription(pht('Parent project to create a milestone for.'))
->setConduitDescription(
pht('Choose a parent project to create a new milestone for.'))
->setConduitTypeDescription(pht('PHID of the parent project.'))
->setAliases(array('milestonePHID'))
->setTransactionType(
PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new AphrontPHIDHTTPParameterType())
->setSingleValue($milestone_phid)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setIsLocked(true),
id(new PhabricatorHandlesEditField())
->setKey('milestone.previous')
->setLabel(pht('Previous Milestone'))
->setSingleValue($previous_milestone_phid)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setIsLocked(true),
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setDescription(pht('Project name.'))
->setConduitDescription(pht('Rename the project'))
->setConduitTypeDescription(pht('New project name.'))
->setValue($object->getName()),
id(new PhabricatorIconSetEditField())
->setKey('icon')
->setLabel(pht('Icon'))
->setTransactionType(
PhabricatorProjectIconTransaction::TRANSACTIONTYPE)
->setIconSet(new PhabricatorProjectIconSet())
->setDescription(pht('Project icon.'))
->setConduitDescription(pht('Change the project icon.'))
->setConduitTypeDescription(pht('New project icon.'))
->setValue($object->getIcon()),
id(new PhabricatorSelectEditField())
->setKey('color')
->setLabel(pht('Color'))
->setTransactionType(
PhabricatorProjectColorTransaction::TRANSACTIONTYPE)
->setOptions(PhabricatorProjectIconSet::getColorMap())
->setDescription(pht('Project tag color.'))
->setConduitDescription(pht('Change the project tag color.'))
->setConduitTypeDescription(pht('New project tag color.'))
->setValue($object->getColor()),
id(new PhabricatorStringListEditField())
->setKey('slugs')
->setLabel(pht('Additional Hashtags'))
->setTransactionType(
PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setDescription(pht('Additional project slugs.'))
->setConduitDescription(pht('Change project slugs.'))
->setConduitTypeDescription(pht('New list of slugs.'))
->setValue($slugs),
);
$can_edit_members = (!$milestone) &&
(!$object->isMilestone()) &&
(!$object->getHasSubprojects());
if ($can_edit_members) {
// Show this on the web UI when creating a project, but not when editing
// one. It is always available via Conduit.
$conduit_only = !$this->getIsCreate();
$members_field = id(new PhabricatorUsersEditField())
->setKey('members')
->setAliases(array('memberPHIDs'))
->setLabel(pht('Initial Members'))
->setIsConduitOnly($conduit_only)
->setUseEdgeTransactions(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST)
->setDescription(pht('Initial project members.'))
->setConduitDescription(pht('Set project members.'))
->setConduitTypeDescription(pht('New list of members.'))
->setValue(array());
$members_field->setViewer($this->getViewer());
$edit_add = $members_field->getConduitEditType('members.add')
->setConduitDescription(pht('Add members.'));
$edit_set = $members_field->getConduitEditType('members.set')
->setConduitDescription(
pht('Set members, overwriting the current value.'));
$edit_rem = $members_field->getConduitEditType('members.remove')
->setConduitDescription(pht('Remove members.'));
$fields[] = $members_field;
}
return $fields;
}
}
diff --git a/src/applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php b/src/applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php
new file mode 100644
index 000000000..25b7cf7c4
--- /dev/null
+++ b/src/applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php
@@ -0,0 +1,29 @@
+<?php
+
+final class PhabricatorProjectsAllPolicyRule
+ extends PhabricatorProjectsBasePolicyRule {
+
+ public function getRuleDescription() {
+ return pht('members of all projects');
+ }
+
+ public function applyRule(
+ PhabricatorUser $viewer,
+ $value,
+ PhabricatorPolicyInterface $object) {
+
+ $memberships = $this->getMemberships($viewer->getPHID());
+ foreach ($value as $project_phid) {
+ if (empty($memberships[$project_phid])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function getRuleOrder() {
+ return 205;
+ }
+
+}
diff --git a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php b/src/applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php
similarity index 75%
copy from src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php
copy to src/applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php
index 3977b542c..fed217779 100644
--- a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php
+++ b/src/applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php
@@ -1,82 +1,64 @@
<?php
-final class PhabricatorProjectsPolicyRule
+abstract class PhabricatorProjectsBasePolicyRule
extends PhabricatorPolicyRule {
private $memberships = array();
- public function getRuleDescription() {
- return pht('members of projects');
+ protected function getMemberships($viewer_phid) {
+ return idx($this->memberships, $viewer_phid, array());
}
public function willApplyRules(
PhabricatorUser $viewer,
array $values,
array $objects) {
$values = array_unique(array_filter(array_mergev($values)));
if (!$values) {
return;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withMemberPHIDs(array($viewer->getPHID()))
->withPHIDs($values)
->execute();
foreach ($projects as $project) {
$this->memberships[$viewer->getPHID()][$project->getPHID()] = true;
}
}
- public function applyRule(
- PhabricatorUser $viewer,
- $value,
- PhabricatorPolicyInterface $object) {
-
- foreach ($value as $project_phid) {
- if (isset($this->memberships[$viewer->getPHID()][$project_phid])) {
- return true;
- }
- }
-
- return false;
- }
-
public function getValueControlType() {
return self::CONTROL_TYPE_TOKENIZER;
}
public function getValueControlTemplate() {
$datasource = id(new PhabricatorProjectDatasource())
->setParameters(
array(
'policy' => 1,
));
return $this->getDatasourceTemplate($datasource);
}
- public function getRuleOrder() {
- return 200;
- }
-
public function getValueForStorage($value) {
PhutilTypeSpec::newFromString('list<string>')->check($value);
return array_values($value);
}
public function getValueForDisplay(PhabricatorUser $viewer, $value) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($value)
->execute();
return mpull($handles, 'getFullName', 'getPHID');
}
public function ruleHasEffect($value) {
return (bool)$value;
}
}
diff --git a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php b/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php
index 3977b542c..b7ad73464 100644
--- a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php
+++ b/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php
@@ -1,82 +1,29 @@
<?php
final class PhabricatorProjectsPolicyRule
- extends PhabricatorPolicyRule {
-
- private $memberships = array();
+ extends PhabricatorProjectsBasePolicyRule {
public function getRuleDescription() {
- return pht('members of projects');
- }
-
- public function willApplyRules(
- PhabricatorUser $viewer,
- array $values,
- array $objects) {
-
- $values = array_unique(array_filter(array_mergev($values)));
- if (!$values) {
- return;
- }
-
- $projects = id(new PhabricatorProjectQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withMemberPHIDs(array($viewer->getPHID()))
- ->withPHIDs($values)
- ->execute();
- foreach ($projects as $project) {
- $this->memberships[$viewer->getPHID()][$project->getPHID()] = true;
- }
+ return pht('members of any project');
}
public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
+ $memberships = $this->getMemberships($viewer->getPHID());
foreach ($value as $project_phid) {
- if (isset($this->memberships[$viewer->getPHID()][$project_phid])) {
+ if (isset($memberships[$project_phid])) {
return true;
}
}
return false;
}
- public function getValueControlType() {
- return self::CONTROL_TYPE_TOKENIZER;
- }
-
- public function getValueControlTemplate() {
- $datasource = id(new PhabricatorProjectDatasource())
- ->setParameters(
- array(
- 'policy' => 1,
- ));
-
- return $this->getDatasourceTemplate($datasource);
- }
-
public function getRuleOrder() {
return 200;
}
- public function getValueForStorage($value) {
- PhutilTypeSpec::newFromString('list<string>')->check($value);
- return array_values($value);
- }
-
- public function getValueForDisplay(PhabricatorUser $viewer, $value) {
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($viewer)
- ->withPHIDs($value)
- ->execute();
-
- return mpull($handles, 'getFullName', 'getPHID');
- }
-
- public function ruleHasEffect($value) {
- return (bool)$value;
- }
-
}
diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php
index e38532c41..02e795395 100644
--- a/src/applications/project/query/PhabricatorProjectSearchEngine.php
+++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php
@@ -1,273 +1,279 @@
<?php
final class PhabricatorProjectSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Projects');
}
public function getApplicationClassName() {
return 'PhabricatorProjectApplication';
}
public function newQuery() {
return id(new PhabricatorProjectQuery())
->needImages(true)
->needMembers(true)
->needWatchers(true);
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchTextField())
->setLabel(pht('Name'))
- ->setKey('name'),
+ ->setKey('name')
+ ->setDescription(
+ pht(
+ '(Deprecated.) Search for projects with a given name or '.
+ 'hashtag using tokenizer/datasource query matching rules. This '.
+ 'is deprecated in favor of the more powerful "query" '.
+ 'constraint.')),
id(new PhabricatorSearchStringListField())
->setLabel(pht('Slugs'))
->setIsHidden(true)
->setKey('slugs')
->setDescription(
pht(
'Search for projects with particular slugs. (Slugs are the same '.
'as project hashtags.)')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Members'))
->setKey('memberPHIDs')
->setConduitKey('members')
->setAliases(array('member', 'members')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Watchers'))
->setKey('watcherPHIDs')
->setConduitKey('watchers')
->setAliases(array('watcher', 'watchers')),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Status'))
->setKey('status')
->setOptions($this->getStatusOptions()),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Milestones'))
->setKey('isMilestone')
->setOptions(
pht('(Show All)'),
pht('Show Only Milestones'),
pht('Hide Milestones'))
->setDescription(
pht(
'Pass true to find only milestones, or false to omit '.
'milestones.')),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Icons'))
->setKey('icons')
->setOptions($this->getIconOptions()),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Colors'))
->setKey('colors')
->setOptions($this->getColorOptions()),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Parent Projects'))
->setKey('parentPHIDs')
->setConduitKey('parents')
->setAliases(array('parent', 'parents', 'parentPHID'))
->setDescription(pht('Find direct subprojects of specified parents.')),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Ancestor Projects'))
->setKey('ancestorPHIDs')
->setConduitKey('ancestors')
->setAliases(array('ancestor', 'ancestors', 'ancestorPHID'))
->setDescription(
pht('Find all subprojects beneath specified ancestors.')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if (strlen($map['name'])) {
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($map['name']);
$query->withNameTokens($tokens);
}
if ($map['slugs']) {
$query->withSlugs($map['slugs']);
}
if ($map['memberPHIDs']) {
$query->withMemberPHIDs($map['memberPHIDs']);
}
if ($map['watcherPHIDs']) {
$query->withWatcherPHIDs($map['watcherPHIDs']);
}
if ($map['status']) {
$status = idx($this->getStatusValues(), $map['status']);
if ($status) {
$query->withStatus($status);
}
}
if ($map['icons']) {
$query->withIcons($map['icons']);
}
if ($map['colors']) {
$query->withColors($map['colors']);
}
if ($map['isMilestone'] !== null) {
$query->withIsMilestone($map['isMilestone']);
}
if ($map['parentPHIDs']) {
$query->withParentProjectPHIDs($map['parentPHIDs']);
}
if ($map['ancestorPHIDs']) {
$query->withAncestorProjectPHIDs($map['ancestorPHIDs']);
}
return $query;
}
protected function getURI($path) {
return '/project/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['joined'] = pht('Joined');
}
if ($this->requireViewer()->isLoggedIn()) {
$names['watching'] = pht('Watching');
}
$names['active'] = pht('Active');
$names['all'] = pht('All');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
// By default, do not show milestones in the list view.
$query->setParameter('isMilestone', false);
switch ($query_key) {
case 'all':
return $query;
case 'active':
return $query
->setParameter('status', 'active');
case 'joined':
return $query
->setParameter('memberPHIDs', array($viewer_phid))
->setParameter('status', 'active');
case 'watching':
return $query
->setParameter('watcherPHIDs', array($viewer_phid))
->setParameter('status', 'active');
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
'active' => pht('Show Only Active Projects'),
'archived' => pht('Show Only Archived Projects'),
'all' => pht('Show All Projects'),
);
}
private function getStatusValues() {
return array(
'active' => PhabricatorProjectQuery::STATUS_ACTIVE,
'archived' => PhabricatorProjectQuery::STATUS_ARCHIVED,
'all' => PhabricatorProjectQuery::STATUS_ANY,
);
}
private function getIconOptions() {
$options = array();
$set = new PhabricatorProjectIconSet();
foreach ($set->getIcons() as $icon) {
if ($icon->getIsDisabled()) {
continue;
}
$options[$icon->getKey()] = array(
id(new PHUIIconView())
->setIcon($icon->getIcon()),
' ',
$icon->getLabel(),
);
}
return $options;
}
private function getColorOptions() {
$options = array();
foreach (PhabricatorProjectIconSet::getColorMap() as $color => $name) {
$options[$color] = array(
id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor($color)
->setName($name),
);
}
return $options;
}
protected function renderResultList(
array $projects,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($projects, 'PhabricatorProject');
$viewer = $this->requireViewer();
$list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($projects)
->setShowWatching(true)
->setShowMember(true)
->renderList();
return id(new PhabricatorApplicationSearchResultView())
->setObjectList($list)
->setNoDataString(pht('No projects found.'));
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Project'))
->setHref('/project/edit/')
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('Projects are flexible storage containers used as '.
'tags, teams, projects, or anything you need to group.'))
->addAction($create_button);
return $view;
}
}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index 4ec4b6367..a049354ad 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,862 +1,887 @@
<?php
final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
- PhabricatorColumnProxyInterface {
+ PhabricatorColumnProxyInterface,
+ PhabricatorSpacesInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $primarySlug;
protected $profileImagePHID;
protected $icon;
protected $color;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
protected $isMembershipLocked;
protected $parentProjectPHID;
protected $hasWorkboard;
protected $hasMilestones;
protected $hasSubprojects;
protected $milestoneNumber;
protected $projectPath;
protected $projectDepth;
protected $projectPathKey;
protected $properties = array();
+ protected $spacePHID;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $slugs = self::ATTACHABLE;
private $parentProject = self::ATTACHABLE;
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
const ITEM_PICTURE = 'project.picture';
const ITEM_PROFILE = 'project.profile';
const ITEM_POINTS = 'project.points';
const ITEM_WORKBOARD = 'project.workboard';
const ITEM_MEMBERS = 'project.members';
const ITEM_MANAGE = 'project.manage';
const ITEM_MILESTONES = 'project.milestones';
const ITEM_SUBPROJECTS = 'project.subprojects';
- public static function initializeNewProject(PhabricatorUser $actor) {
+ public static function initializeNewProject(
+ PhabricatorUser $actor,
+ PhabricatorProject $parent = null) {
+
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorProjectApplication'))
->executeOne();
$view_policy = $app->getPolicy(
ProjectDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
ProjectDefaultEditCapability::CAPABILITY);
$join_policy = $app->getPolicy(
ProjectDefaultJoinCapability::CAPABILITY);
+ // If this is the child of some other project, default the Space to the
+ // Space of the parent.
+ if ($parent) {
+ $space_phid = $parent->getSpacePHID();
+ } else {
+ $space_phid = $actor->getDefaultSpacePHID();
+ }
+
$default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
$default_color = PhabricatorProjectIconSet::getDefaultColorKey();
return id(new PhabricatorProject())
->setAuthorPHID($actor->getPHID())
->setIcon($default_icon)
->setColor($default_color)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setJoinPolicy($join_policy)
+ ->setSpacePHID($space_phid)
->setIsMembershipLocked(0)
->attachMemberPHIDs(array())
->attachSlugs(array())
->setHasWorkboard(0)
->setHasMilestones(0)
->setHasSubprojects(0)
->attachParentProject(null);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
if ($this->isMilestone()) {
return $this->getParentProject()->getPolicy($capability);
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isMilestone()) {
return $this->getParentProject()->hasAutomaticCapability(
$capability,
$viewer);
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$parent = $this->getParentProject();
if ($parent) {
$can_edit_parent = PhabricatorPolicyFilter::hasCapability(
$viewer,
$parent,
$can_edit);
if ($can_edit_parent) {
return true;
}
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
// TODO: Clarify the additional rules that parent and subprojects imply.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Members of a project can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht('Users who can edit a project can always join it.');
}
return null;
}
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$parent = $this->getParentProject();
if ($parent) {
$extended[] = array(
$parent,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
public function isUserMember($user_phid) {
if ($this->memberPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->memberPHIDs);
}
return $this->assertAttachedKey($this->sparseMembers, $user_phid);
}
public function setIsUserMember($user_phid, $is_member) {
if ($this->sparseMembers === self::ATTACHABLE) {
$this->sparseMembers = array();
}
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'status' => 'text32',
'primarySlug' => 'text128?',
'isMembershipLocked' => 'bool',
'profileImagePHID' => 'phid?',
'icon' => 'text32',
'color' => 'text32',
'mailKey' => 'bytes20',
'joinPolicy' => 'policy',
'parentProjectPHID' => 'phid?',
'hasWorkboard' => 'bool',
'hasMilestones' => 'bool',
'hasSubprojects' => 'bool',
'milestoneNumber' => 'uint32?',
'projectPath' => 'hashpath64',
'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4',
),
self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array(
'columns' => array('icon'),
),
'key_color' => array(
'columns' => array('color'),
),
'key_milestone' => array(
'columns' => array('parentProjectPHID', 'milestoneNumber'),
'unique' => true,
),
'key_primaryslug' => array(
'columns' => array('primarySlug'),
'unique' => true,
),
'key_path' => array(
'columns' => array('projectPath', 'projectDepth'),
),
'key_pathkey' => array(
'columns' => array('projectPathKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectProjectPHIDType::TYPECONST);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function isUserWatcher($user_phid) {
if ($this->watcherPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->watcherPHIDs);
}
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
}
public function isUserAncestorWatcher($user_phid) {
$is_watcher = $this->isUserWatcher($user_phid);
if (!$is_watcher) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->isUserWatcher($user_phid);
}
}
return $is_watcher;
}
public function getWatchedAncestorPHID($user_phid) {
if ($this->isUserWatcher($user_phid)) {
return $this->getPHID();
}
$parent = $this->getParentProject();
if ($parent) {
return $parent->getWatchedAncestorPHID($user_phid);
}
return null;
}
public function setIsUserWatcher($user_phid, $is_watcher) {
if ($this->sparseWatchers === self::ATTACHABLE) {
$this->sparseWatchers = array();
}
$this->sparseWatchers[$user_phid] = $is_watcher;
return $this;
}
public function attachWatcherPHIDs(array $phids) {
$this->watcherPHIDs = $phids;
return $this;
}
public function getWatcherPHIDs() {
return $this->assertAttached($this->watcherPHIDs);
}
public function getAllAncestorWatcherPHIDs() {
$parent = $this->getParentProject();
if ($parent) {
$watchers = $parent->getAllAncestorWatcherPHIDs();
} else {
$watchers = array();
}
foreach ($this->getWatcherPHIDs() as $phid) {
$watchers[$phid] = $phid;
}
return $watchers;
}
public function attachSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function getSlugs() {
return $this->assertAttached($this->slugs);
}
public function getColor() {
if ($this->isArchived()) {
return PHUITagView::COLOR_DISABLED;
}
return $this->color;
}
public function getURI() {
$id = $this->getID();
return "/project/view/{$id}/";
}
public function getProfileURI() {
$id = $this->getID();
return "/project/profile/{$id}/";
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
if (!strlen($this->getPHID())) {
$this->setPHID($this->generatePHID());
}
if (!strlen($this->getProjectPathKey())) {
$hash = PhabricatorHash::digestForIndex($this->getPHID());
$hash = substr($hash, 0, 4);
$this->setProjectPathKey($hash);
}
$path = array();
$depth = 0;
if ($this->parentProjectPHID) {
$parent = $this->getParentProject();
$path[] = $parent->getProjectPath();
$depth = $parent->getProjectDepth() + 1;
}
$path[] = $this->getProjectPathKey();
$path = implode('', $path);
$limit = self::getProjectDepthLimit();
if ($depth >= $limit) {
throw new Exception(pht('Project depth is too great.'));
}
$this->setProjectPath($path);
$this->setProjectDepth($depth);
$this->openTransaction();
$result = parent::save();
$this->updateDatasourceTokens();
$this->saveTransaction();
return $result;
}
public static function getProjectDepthLimit() {
// This is limited by how many path hashes we can fit in the path
// column.
return 16;
}
public function updateDatasourceTokens() {
$table = self::TABLE_DATASOURCE_TOKEN;
$conn_w = $this->establishConnection('w');
$id = $this->getID();
$slugs = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE projectPHID = %s',
id(new PhabricatorProjectSlug())->getTableName(),
$this->getPHID());
$all_strings = ipull($slugs, 'slug');
$all_strings[] = $this->getDisplayName();
$all_strings = implode(' ', $all_strings);
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
}
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE projectID = %d',
$table,
$id);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (projectID, token) VALUES %Q',
$table,
$chunk);
}
$this->saveTransaction();
}
public function isMilestone() {
return ($this->getMilestoneNumber() !== null);
}
public function getParentProject() {
return $this->assertAttached($this->parentProject);
}
public function attachParentProject(PhabricatorProject $project = null) {
$this->parentProject = $project;
return $this;
}
public function getAncestorProjectPaths() {
$parts = array();
$path = $this->getProjectPath();
$parent_length = (strlen($path) - 4);
for ($ii = $parent_length; $ii > 0; $ii -= 4) {
$parts[] = substr($path, 0, $ii);
}
return $parts;
}
public function getAncestorProjects() {
$ancestors = array();
$cursor = $this->getParentProject();
while ($cursor) {
$ancestors[] = $cursor;
$cursor = $cursor->getParentProject();
}
return $ancestors;
}
public function supportsEditMembers() {
if ($this->isMilestone()) {
return false;
}
if ($this->getHasSubprojects()) {
return false;
}
return true;
}
public function supportsMilestones() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function supportsSubprojects() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function loadNextMilestoneNumber() {
$current = queryfx_one(
$this->establishConnection('w'),
'SELECT MAX(milestoneNumber) n
FROM %T
WHERE parentProjectPHID = %s',
$this->getTableName(),
$this->getPHID());
if (!$current) {
$number = 1;
} else {
$number = (int)$current['n'] + 1;
}
return $number;
}
public function getDisplayName() {
$name = $this->getName();
// If this is a milestone, show it as "Parent > Sprint 99".
if ($this->isMilestone()) {
$name = pht(
'%s (%s)',
$this->getParentProject()->getName(),
$name);
}
return $name;
}
public function getDisplayIconKey() {
if ($this->isMilestone()) {
$key = PhabricatorProjectIconSet::getMilestoneIconKey();
} else {
$key = $this->getIcon();
}
return $key;
}
public function getDisplayIconIcon() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconIcon($key);
}
public function getDisplayIconName() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconName($key);
}
public function getDisplayColor() {
if ($this->isMilestone()) {
return $this->getParentProject()->getColor();
}
return $this->getColor();
}
public function getDisplayIconComposeIcon() {
$icon = $this->getDisplayIconIcon();
return $icon;
}
public function getDisplayIconComposeColor() {
$color = $this->getDisplayColor();
$map = array(
'grey' => 'charcoal',
'checkered' => 'backdrop',
);
return idx($map, $color, $color);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getDefaultWorkboardSort() {
return $this->getProperty('workboard.sort.default');
}
public function setDefaultWorkboardSort($sort) {
return $this->setProperty('workboard.sort.default', $sort);
}
public function getDefaultWorkboardFilter() {
return $this->getProperty('workboard.filter.default');
}
public function setDefaultWorkboardFilter($filter) {
return $this->setProperty('workboard.filter.default', $filter);
}
public function getWorkboardBackgroundColor() {
return $this->getProperty('workboard.background');
}
public function setWorkboardBackgroundColor($color) {
return $this->setProperty('workboard.background', $color);
}
public function getDisplayWorkboardBackgroundColor() {
$color = $this->getWorkboardBackgroundColor();
if ($color === null) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->getDisplayWorkboardBackgroundColor();
}
}
if ($color === 'none') {
$color = null;
}
return $color;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
+/* -( PhabricatorSpacesInterface )----------------------------------------- */
+
+
+ public function getSpacePHID() {
+ if ($this->isMilestone()) {
+ return $this->getParentProject()->getSpacePHID();
+ }
+ return $this->spacePHID;
+ }
+
+
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$columns = id(new PhabricatorProjectColumn())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($columns as $column) {
$engine->destroyObject($column);
}
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($slugs as $slug) {
$slug->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorProjectFulltextEngine();
}
/* -( PhabricatorFerretInterface )--------------------------------------- */
public function newFerretEngine() {
return new PhabricatorProjectFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone')
->setType('int?')
->setDescription(pht('For milestones, milestone sequence number.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('parent')
->setType('map<string, wild>?')
->setDescription(
pht(
'For subprojects and milestones, a brief description of the '.
'parent project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('depth')
->setType('int')
->setDescription(
pht(
'For subprojects and milestones, depth of this project in the '.
'tree. Root projects have depth 0.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('icon')
->setType('map<string, wild>')
->setDescription(pht('Information about the project icon.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('color')
->setType('map<string, wild>')
->setDescription(pht('Information about the project color.')),
);
}
public function getFieldValuesForConduit() {
$color_key = $this->getColor();
$color_name = PhabricatorProjectIconSet::getColorName($color_key);
if ($this->isMilestone()) {
$milestone = (int)$this->getMilestoneNumber();
} else {
$milestone = null;
}
$parent = $this->getParentProject();
if ($parent) {
$parent_ref = $parent->getRefForConduit();
} else {
$parent_ref = null;
}
return array(
'name' => $this->getName(),
'slug' => $this->getPrimarySlug(),
'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref,
'icon' => array(
'key' => $this->getDisplayIconKey(),
'name' => $this->getDisplayIconName(),
'icon' => $this->getDisplayIconIcon(),
),
'color' => array(
'key' => $color_key,
'name' => $color_name,
),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorProjectsMembersSearchEngineAttachment())
->setAttachmentKey('members'),
id(new PhabricatorProjectsWatchersSearchEngineAttachment())
->setAttachmentKey('watchers'),
id(new PhabricatorProjectsAncestorsSearchEngineAttachment())
->setAttachmentKey('ancestors'),
);
}
/**
* Get an abbreviated representation of this project for use in providing
* "parent" and "ancestor" information.
*/
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getName(),
);
}
/* -( PhabricatorColumnProxyInterface )------------------------------------ */
public function getProxyColumnName() {
return $this->getName();
}
public function getProxyColumnIcon() {
return $this->getDisplayIconIcon();
}
public function getProxyColumnClass() {
if ($this->isMilestone()) {
return 'phui-workboard-column-milestone';
}
return null;
}
}
diff --git a/src/applications/project/view/PhabricatorProjectListView.php b/src/applications/project/view/PhabricatorProjectListView.php
index 7496a401b..645440d83 100644
--- a/src/applications/project/view/PhabricatorProjectListView.php
+++ b/src/applications/project/view/PhabricatorProjectListView.php
@@ -1,99 +1,100 @@
<?php
final class PhabricatorProjectListView extends AphrontView {
private $projects;
private $showMember;
private $showWatching;
private $noDataString;
public function setProjects(array $projects) {
$this->projects = $projects;
return $this;
}
public function getProjects() {
return $this->projects;
}
public function setShowWatching($watching) {
$this->showWatching = $watching;
return $this;
}
public function setShowMember($member) {
$this->showMember = $member;
return $this;
}
public function setNoDataString($text) {
$this->noDataString = $text;
return $this;
}
public function renderList() {
$viewer = $this->getUser();
$viewer_phid = $viewer->getPHID();
$projects = $this->getProjects();
$handles = $viewer->loadHandles(mpull($projects, 'getPHID'));
$no_data = pht('No projects found.');
if ($this->noDataString) {
$no_data = $this->noDataString;
}
$list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString($no_data);
foreach ($projects as $key => $project) {
$id = $project->getID();
$icon = $project->getDisplayIconIcon();
$icon_icon = id(new PHUIIconView())
->setIcon($icon);
$icon_name = $project->getDisplayIconName();
$item = id(new PHUIObjectItemView())
+ ->setObject($project)
->setHeader($project->getName())
->setHref("/project/view/{$id}/")
->setImageURI($project->getProfileImageURI())
->addAttribute(
array(
$icon_icon,
' ',
$icon_name,
));
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
$item->addIcon('fa-ban', pht('Archived'));
$item->setDisabled(true);
}
if ($this->showMember) {
$is_member = $project->isUserMember($viewer_phid);
if ($is_member) {
$item->addIcon('fa-user', pht('Member'));
}
}
if ($this->showWatching) {
$is_watcher = $project->isUserWatcher($viewer_phid);
if ($is_watcher) {
$item->addIcon('fa-eye', pht('Watching'));
}
}
$list->addItem($item);
}
return $list;
}
public function render() {
return $this->renderList();
}
}
diff --git a/src/applications/project/xaction/PhabricatorProjectLockTransaction.php b/src/applications/project/xaction/PhabricatorProjectLockTransaction.php
index 42551dfb2..c54c9284c 100644
--- a/src/applications/project/xaction/PhabricatorProjectLockTransaction.php
+++ b/src/applications/project/xaction/PhabricatorProjectLockTransaction.php
@@ -1,56 +1,64 @@
<?php
final class PhabricatorProjectLockTransaction
extends PhabricatorProjectTransactionType {
const TRANSACTIONTYPE = 'project:locked';
public function generateOldValue($object) {
return (int)$object->getIsMembershipLocked();
}
public function applyInternalEffects($object, $value) {
$object->setIsMembershipLocked($value);
}
public function getTitle() {
$new = $this->getNewValue();
if ($new) {
return pht(
"%s locked this project's membership.",
$this->renderAuthor());
} else {
return pht(
"%s unlocked this project's membership.",
$this->renderAuthor());
}
}
public function getTitleForFeed() {
$new = $this->getNewValue();
if ($new) {
return pht(
'%s locked %s membership.',
$this->renderAuthor(),
$this->renderObject());
} else {
return pht(
'%s unlocked %s membership.',
$this->renderAuthor(),
$this->renderObject());
}
}
public function getIcon() {
$new = $this->getNewValue();
if ($new) {
return 'fa-lock';
} else {
return 'fa-unlock';
}
}
+ public function validateTransactions($object, array $xactions) {
+ if ($xactions) {
+ $this->requireApplicationCapability(
+ ProjectCanLockProjectsCapability::CAPABILITY);
+ }
+ return array();
+ }
+
}
diff --git a/src/applications/repository/editor/PhabricatorRepositoryEditor.php b/src/applications/repository/editor/PhabricatorRepositoryEditor.php
index 768121de5..882c1a11f 100644
--- a/src/applications/repository/editor/PhabricatorRepositoryEditor.php
+++ b/src/applications/repository/editor/PhabricatorRepositoryEditor.php
@@ -1,652 +1,618 @@
<?php
final class PhabricatorRepositoryEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getEditorObjectsDescription() {
return pht('Repositories');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorRepositoryTransaction::TYPE_VCS;
$types[] = PhabricatorRepositoryTransaction::TYPE_ACTIVATE;
$types[] = PhabricatorRepositoryTransaction::TYPE_NAME;
$types[] = PhabricatorRepositoryTransaction::TYPE_DESCRIPTION;
$types[] = PhabricatorRepositoryTransaction::TYPE_ENCODING;
$types[] = PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH;
$types[] = PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY;
$types[] = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY;
$types[] = PhabricatorRepositoryTransaction::TYPE_UUID;
$types[] = PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH;
$types[] = PhabricatorRepositoryTransaction::TYPE_NOTIFY;
$types[] = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE;
$types[] = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY;
$types[] = PhabricatorRepositoryTransaction::TYPE_DANGEROUS;
$types[] = PhabricatorRepositoryTransaction::TYPE_ENORMOUS;
$types[] = PhabricatorRepositoryTransaction::TYPE_SLUG;
$types[] = PhabricatorRepositoryTransaction::TYPE_SERVICE;
$types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE;
$types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES;
$types[] = PhabricatorRepositoryTransaction::TYPE_STAGING_URI;
$types[] = PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS;
$types[] = PhabricatorRepositoryTransaction::TYPE_CALLSIGN;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryTransaction::TYPE_VCS:
return $object->getVersionControlSystem();
case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
return $object->isTracked();
case PhabricatorRepositoryTransaction::TYPE_NAME:
return $object->getName();
case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
return $object->getDetail('description');
case PhabricatorRepositoryTransaction::TYPE_ENCODING:
return $object->getDetail('encoding');
case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
return $object->getDetail('default-branch');
case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
return array_keys($object->getDetail('branch-filter', array()));
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
return array_keys($object->getDetail('close-commits-filter', array()));
case PhabricatorRepositoryTransaction::TYPE_UUID:
return $object->getUUID();
case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
return $object->getDetail('svn-subpath');
case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
return (int)!$object->getDetail('herald-disabled');
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
return (int)!$object->getDetail('disable-autoclose');
case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
return $object->getPushPolicy();
case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
return $object->shouldAllowDangerousChanges();
case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
return $object->shouldAllowEnormousChanges();
case PhabricatorRepositoryTransaction::TYPE_SLUG:
return $object->getRepositorySlug();
case PhabricatorRepositoryTransaction::TYPE_SERVICE:
return $object->getAlmanacServicePHID();
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
return $object->getSymbolLanguages();
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
return $object->getSymbolSources();
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
return $object->getDetail('staging-uri');
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
return $object->getDetail('automation.blueprintPHIDs', array());
case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
return $object->getCallsign();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
case PhabricatorRepositoryTransaction::TYPE_NAME:
case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
case PhabricatorRepositoryTransaction::TYPE_ENCODING:
case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
case PhabricatorRepositoryTransaction::TYPE_UUID:
case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
case PhabricatorRepositoryTransaction::TYPE_VCS:
case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
case PhabricatorRepositoryTransaction::TYPE_SERVICE:
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
return $xaction->getNewValue();
case PhabricatorRepositoryTransaction::TYPE_SLUG:
case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
$name = $xaction->getNewValue();
if (strlen($name)) {
return $name;
}
return null;
case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
return (int)$xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryTransaction::TYPE_VCS:
$object->setVersionControlSystem($xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
$active = $xaction->getNewValue();
// The first time a repository is activated, clear the "new repository"
// flag so we stop showing setup hints.
if ($active) {
$object->setDetail('newly-initialized', false);
}
$object->setDetail('tracking-enabled', $active);
break;
case PhabricatorRepositoryTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
$object->setDetail('description', $xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
$object->setDetail('default-branch', $xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
$object->setDetail(
'branch-filter',
array_fill_keys($xaction->getNewValue(), true));
break;
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
$object->setDetail(
'close-commits-filter',
array_fill_keys($xaction->getNewValue(), true));
break;
case PhabricatorRepositoryTransaction::TYPE_UUID:
$object->setUUID($xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
$object->setDetail('svn-subpath', $xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
$object->setDetail('herald-disabled', (int)!$xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
$object->setDetail('disable-autoclose', (int)!$xaction->getNewValue());
break;
case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
return $object->setPushPolicy($xaction->getNewValue());
case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
$object->setDetail('allow-dangerous-changes', $xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
$object->setDetail('allow-enormous-changes', $xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_SLUG:
$object->setRepositorySlug($xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_SERVICE:
$object->setAlmanacServicePHID($xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
$object->setDetail('symbol-languages', $xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
$object->setDetail('symbol-sources', $xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
$object->setDetail('staging-uri', $xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
$object->setDetail(
'automation.blueprintPHIDs',
$xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
$object->setCallsign($xaction->getNewValue());
return;
case PhabricatorRepositoryTransaction::TYPE_ENCODING:
$object->setDetail('encoding', $xaction->getNewValue());
break;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
DrydockAuthorization::applyAuthorizationChanges(
$this->getActor(),
$object->getPHID(),
$xaction->getOldValue(),
$xaction->getNewValue());
break;
}
}
- protected function requireCapabilities(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
- case PhabricatorRepositoryTransaction::TYPE_NAME:
- case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
- case PhabricatorRepositoryTransaction::TYPE_ENCODING:
- case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
- case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
- case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
- case PhabricatorRepositoryTransaction::TYPE_UUID:
- case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
- case PhabricatorRepositoryTransaction::TYPE_VCS:
- case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
- case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
- case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
- case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
- case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
- case PhabricatorRepositoryTransaction::TYPE_SLUG:
- case PhabricatorRepositoryTransaction::TYPE_SERVICE:
- case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
- case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
- case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
- case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
- PhabricatorPolicyFilter::requireCapability(
- $this->requireActor(),
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- break;
- }
- }
-
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
foreach ($xactions as $xaction) {
foreach ($xaction->getNewValue() as $pattern) {
// Check for invalid regular expressions.
$regexp = PhabricatorRepository::extractBranchRegexp($pattern);
if ($regexp !== null) {
$ok = @preg_match($regexp, '');
if ($ok === false) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Expression "%s" is not a valid regular expression. Note '.
'that you must include delimiters.',
$regexp),
$xaction);
$errors[] = $error;
continue;
}
}
// Check for formatting mistakes like `regex(...)` instead of
// `regexp(...)`.
$matches = null;
if (preg_match('/^([^(]+)\\(.*\\)\z/', $pattern, $matches)) {
switch ($matches[1]) {
case 'regexp':
break;
default:
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Matching function "%s(...)" is not recognized. Valid '.
'functions are: regexp(...).',
$matches[1]),
$xaction);
$errors[] = $error;
break;
}
}
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
foreach ($xactions as $xaction) {
$old = nonempty($xaction->getOldValue(), array());
$new = nonempty($xaction->getNewValue(), array());
$add = array_diff($new, $old);
$invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer(
$this->getActor(),
$add);
if ($invalid) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Some of the selected automation blueprints are invalid '.
'or restricted: %s.',
implode(', ', $invalid)),
$xaction);
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_VCS:
$vcs_map = PhabricatorRepositoryType::getAllRepositoryTypes();
$current_vcs = $object->getVersionControlSystem();
if (!$this->getIsNewObject()) {
foreach ($xactions as $xaction) {
if ($xaction->getNewValue() == $current_vcs) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Immutable'),
pht(
'You can not change the version control system an existing '.
'repository uses. It can only be set when a repository is '.
'first created.'),
$xaction);
}
} else {
$value = $object->getVersionControlSystem();
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (empty($vcs_map[$value])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Specified version control system must be a VCS '.
'recognized by Phabricator: %s.',
implode(', ', array_keys($vcs_map))),
$xaction);
}
}
if (!strlen($value)) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht(
'When creating a repository, you must specify a valid '.
'underlying version control system: %s.',
implode(', ', array_keys($vcs_map))),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Repository name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
$status_map = PhabricatorRepository::getStatusMap();
foreach ($xactions as $xaction) {
$status = $xaction->getNewValue();
if (empty($status_map[$status])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Repository status "%s" is not valid.',
$status),
$xaction);
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_ENCODING:
foreach ($xactions as $xaction) {
// Make sure the encoding is valid by converting to UTF-8. This tests
// that the user has mbstring installed, and also that they didn't
// type a garbage encoding name. Note that we're converting from
// UTF-8 to the target encoding, because mbstring is fine with
// converting from a nonsense encoding.
$encoding = $xaction->getNewValue();
if (!strlen($encoding)) {
continue;
}
try {
phutil_utf8_convert('.', $encoding, 'UTF-8');
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Repository encoding "%s" is not valid: %s',
$encoding,
$ex->getMessage()),
$xaction);
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_SLUG:
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!strlen($new)) {
continue;
}
if ($new === $old) {
continue;
}
try {
PhabricatorRepository::assertValidRepositorySlug($new);
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
continue;
}
$other = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs(array($new))
->executeOne();
if ($other && ($other->getID() !== $object->getID())) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Duplicate'),
pht(
'The selected repository short name is already in use by '.
'another repository. Choose a unique short name.'),
$xaction);
continue;
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!strlen($new)) {
continue;
}
if ($new === $old) {
continue;
}
try {
PhabricatorRepository::assertValidCallsign($new);
} catch (Exception $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
continue;
}
$other = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withCallsigns(array($new))
->executeOne();
if ($other && ($other->getID() !== $object->getID())) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Duplicate'),
pht(
'The selected callsign ("%s") is already in use by another '.
'repository. Choose a unique callsign.',
$new),
$xaction);
continue;
}
}
break;
case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
foreach ($xactions as $xaction) {
$old = $object->getSymbolSources();
$new = $xaction->getNewValue();
// If the viewer is adding new repositories, make sure they are
// valid and visible.
$add = array_diff($new, $old);
if (!$add) {
continue;
}
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getActor())
->withPHIDs($add)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($add as $phid) {
if (isset($repositories[$phid])) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'Repository ("%s") does not exist, or you do not have '.
'permission to see it.',
$phid),
$xaction);
break;
}
}
break;
}
return $errors;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
$errors = array();
$errors[] = new PhabricatorApplicationTransactionValidationError(
null,
pht('Invalid'),
pht(
'The chosen callsign or repository short name is already in '.
'use by another repository.'),
null);
throw new PhabricatorApplicationTransactionValidationException($errors);
}
protected function supportsSearch() {
return true;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// If the repository does not have a local path yet, assign it one based
// on its ID. We can't do this earlier because we won't have an ID yet.
$local_path = $object->getLocalPath();
if (!strlen($local_path)) {
$local_key = 'repository.default-local-path';
$local_root = PhabricatorEnv::getEnvConfig($local_key);
$local_root = rtrim($local_root, '/');
$id = $object->getID();
$local_path = "{$local_root}/{$id}/";
$object->setLocalPath($local_path);
$object->save();
}
if ($this->getIsNewObject()) {
// The default state of repositories is to be hosted, if they are
// enabled without configuring any "Observe" URIs.
$object->setHosted(true);
$object->save();
// Create this repository's builtin URIs.
$builtin_uris = $object->newBuiltinURIs();
foreach ($builtin_uris as $uri) {
$uri->save();
}
id(new DiffusionRepositoryClusterEngine())
->setViewer($this->getActor())
->setRepository($object)
->synchronizeWorkingCopyAfterCreation();
}
$object->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
null);
return $xactions;
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php b/src/applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php
new file mode 100644
index 000000000..742e4a159
--- /dev/null
+++ b/src/applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php
@@ -0,0 +1,91 @@
+<?php
+
+final class PhabricatorRepositoryIdentityEditEngine
+ extends PhabricatorEditEngine {
+
+ const ENGINECONST = 'repository.identity';
+
+ public function isEngineConfigurable() {
+ return false;
+ }
+
+ public function getEngineName() {
+ return pht('Repository Identities');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Edit Repository Identity Configurations');
+ }
+
+ public function getSummaryText() {
+ return pht('This engine is used to edit Repository identities.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorDiffusionApplication';
+ }
+
+ protected function newEditableObject() {
+ return new PhabricatorRepositoryIdentity();
+ }
+
+ protected function newObjectQuery() {
+ return new PhabricatorRepositoryIdentityQuery();
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create Identity');
+ }
+
+ protected function getObjectCreateButtonText($object) {
+ return pht('Create Identity');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit Identity: %s', $object->getIdentityShortName());
+ }
+
+ protected function getObjectEditShortText($object) {
+ return pht('Edit Identity');
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('Create Identity');
+ }
+
+ protected function getObjectName() {
+ return pht('Identity');
+ }
+
+ protected function getEditorURI() {
+ return '/diffusion/identity/edit/';
+ }
+
+ protected function getObjectCreateCancelURI($object) {
+ return '/diffusion/identity/';
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function getCreateNewObjectPolicy() {
+ return PhabricatorPolicies::POLICY_USER;
+ }
+
+ protected function buildCustomEditFields($object) {
+ return array(
+ id(new DiffusionIdentityAssigneeEditField())
+ ->setKey('manuallySetUserPHID')
+ ->setLabel(pht('Assigned To'))
+ ->setDescription(pht('Override this identity\'s assignment.'))
+ ->setTransactionType(
+ PhabricatorRepositoryIdentityAssignTransaction::TRANSACTIONTYPE)
+ ->setIsCopyable(true)
+ ->setIsNullable(true)
+ ->setSingleValue($object->getManuallySetUserPHID()),
+
+ );
+ }
+
+}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php
new file mode 100644
index 000000000..86cdcaa46
--- /dev/null
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php
@@ -0,0 +1,125 @@
+<?php
+
+final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow
+ extends PhabricatorRepositoryManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('rebuild-identities')
+ ->setExamples(
+ '**rebuild-identities** [__options__] __repository__')
+ ->setSynopsis(pht('Rebuild repository identities from commits.'))
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'repositories',
+ 'wildcard' => true,
+ ),
+ array(
+ 'name' => 'all',
+ 'help' => pht('Rebuild identities across all repositories.'),
+ ),
+ ));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $console = PhutilConsole::getConsole();
+
+ $all = $args->getArg('all');
+ $repositories = $args->getArg('repositories');
+
+ if ($all xor empty($repositories)) {
+ throw new PhutilArgumentUsageException(
+ pht('Specify --all or a list of repositories, but not both.'));
+ }
+
+ $query = id(new DiffusionCommitQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->needCommitData(true);
+
+ if ($repositories) {
+ $repos = $this->loadRepositories($args, 'repositories');
+ $query->withRepositoryIDs(mpull($repos, 'getID'));
+ }
+
+ $iterator = new PhabricatorQueryIterator($query);
+ foreach ($iterator as $commit) {
+ $needs_update = false;
+
+ $data = $commit->getCommitData();
+ $author_name = $data->getAuthorName();
+
+ $author_identity = $this->getIdentityForCommit(
+ $commit,
+ $author_name);
+
+ $author_phid = $commit->getAuthorIdentityPHID();
+ $identity_phid = $author_identity->getPHID();
+ if ($author_phid !== $identity_phid) {
+ $commit->setAuthorIdentityPHID($identity_phid);
+ $data->setCommitDetail('authorIdentityPHID', $identity_phid);
+ $needs_update = true;
+ }
+
+ $committer_name = $data->getCommitDetail('committer', null);
+ $committer_phid = $commit->getCommitterIdentityPHID();
+ if (strlen($committer_name)) {
+ $committer_identity = $this->getIdentityForCommit(
+ $commit,
+ $committer_name);
+ $identity_phid = $committer_identity->getPHID();
+ } else {
+ $identity_phid = null;
+ }
+
+ if ($committer_phid !== $identity_phid) {
+ $commit->setCommitterIdentityPHID($identity_phid);
+ $data->setCommitDetail('committerIdentityPHID', $identity_phid);
+ $needs_update = true;
+ }
+
+ if ($needs_update) {
+ $commit->save();
+ $data->save();
+ echo tsprintf(
+ "Rebuilt identities for %s.\n",
+ $commit->getDisplayName());
+ } else {
+ echo tsprintf(
+ "No changes for %s.\n",
+ $commit->getDisplayName());
+ }
+ }
+
+ }
+
+ private function getIdentityForCommit(
+ PhabricatorRepositoryCommit $commit, $identity_name) {
+
+ static $seen = array();
+ $identity_key = PhabricatorHash::digestForIndex($identity_name);
+ if (empty($seen[$identity_key])) {
+ try {
+ $user_phid = id(new DiffusionResolveUserQuery())
+ ->withCommit($commit)
+ ->withName($identity_name)
+ ->execute();
+
+ $identity = id(new PhabricatorRepositoryIdentity())
+ ->setAuthorPHID($commit->getPHID())
+ ->setIdentityName($identity_name)
+ ->setAutomaticGuessedUserPHID($user_phid)
+ ->save();
+ } catch (AphrontDuplicateKeyQueryException $ex) {
+ // Somehow this identity already exists?
+ $identity = id(new PhabricatorRepositoryIdentityQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withIdentityNames(array($identity_name))
+ ->executeOne();
+ }
+ $seen[$identity_key] = $identity;
+ }
+
+ return $seen[$identity_key];
+ }
+}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php
index 2067fdbd1..8b8907521 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php
@@ -1,337 +1,337 @@
<?php
final class PhabricatorRepositoryManagementReparseWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('reparse')
- ->setExamples('**reparse** [options] __repository__')
+ ->setExamples('**reparse** [options] __commit__')
->setSynopsis(
pht(
'**reparse** __what__ __which_parts__ [--trace] [--force]'."\n\n".
'Rerun the Diffusion parser on specific commits and repositories. '.
'Mostly useful for debugging changes to Diffusion.'."\n\n".
'e.g. enqueue reparse owners in the TEST repo for all commits:'."\n".
'repository reparse --all TEST --owners'."\n\n".
'e.g. do same but exclude before yesterday (local time):'."\n".
'repository reparse --all TEST --owners --min-date yesterday'."\n".
'repository reparse --all TEST --owners --min-date "today -1 day".'.
"\n\n".
'e.g. do same but exclude before 03/31/2013 (local time):'."\n".
'repository reparse --all TEST --owners --min-date "03/31/2013"'))
->setArguments(
array(
array(
'name' => 'revision',
'wildcard' => true,
),
array(
'name' => 'all',
'param' => 'repository',
'help' => pht(
'Reparse all commits in the specified repository. This mode '.
'queues parsers into the task queue; you must run taskmasters '.
'to actually do the parses. Use with __%s__ to run '.
'the tasks locally instead of with taskmasters.',
'--force-local'),
),
array(
'name' => 'min-date',
'param' => 'date',
'help' => pht(
"Must be used with __%s__, this will exclude commits which ".
"are earlier than __date__.\n".
"Valid examples:\n".
" 'today', 'today 2pm', '-1 hour', '-2 hours', '-24 hours',\n".
" 'yesterday', 'today -1 day', 'yesterday 2pm', '2pm -1 day',\n".
" 'last Monday', 'last Monday 14:00', 'last Monday 2pm',\n".
" '31 March 2013', '31 Mar', '03/31', '03/31/2013',\n".
"See __%s__ for more.",
'--all',
'http://www.php.net/manual/en/datetime.formats.php'),
),
array(
'name' => 'message',
'help' => pht('Reparse commit messages.'),
),
array(
'name' => 'change',
'help' => pht('Reparse changes.'),
),
array(
'name' => 'herald',
'help' => pht(
'Reevaluate Herald rules (may send huge amounts of email!)'),
),
array(
'name' => 'owners',
'help' => pht(
'Reevaluate related commits for owners packages (may delete '.
'existing relationship entries between your package and some '.
'old commits!)'),
),
array(
'name' => 'force',
'short' => 'f',
'help' => pht('Act noninteractively, without prompting.'),
),
array(
'name' => 'force-local',
'help' => pht(
'Only used with __%s__, use this to run the tasks locally '.
'instead of deferring them to taskmaster daemons.',
'--all'),
),
array(
'name' => 'importing',
'help' => pht(
'Reparse all steps which have not yet completed.'),
),
array(
'name' => 'force-autoclose',
'help' => pht(
'Only used with __%s__, use this to make sure any '.
'pertinent diffs are closed regardless of configuration.',
'--message'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$all_from_repo = $args->getArg('all');
$reparse_message = $args->getArg('message');
$reparse_change = $args->getArg('change');
$reparse_herald = $args->getArg('herald');
$reparse_owners = $args->getArg('owners');
$reparse_what = $args->getArg('revision');
$force = $args->getArg('force');
$force_local = $args->getArg('force-local');
$min_date = $args->getArg('min-date');
$importing = $args->getArg('importing');
if (!$all_from_repo && !$reparse_what) {
throw new PhutilArgumentUsageException(
pht('Specify a commit or repository to reparse.'));
}
if ($all_from_repo && $reparse_what) {
$commits = implode(', ', $reparse_what);
throw new PhutilArgumentUsageException(
pht(
"Specify a commit or repository to reparse, not both:\n".
"All from repo: %s\n".
"Commit(s) to reparse: %s",
$all_from_repo,
$commits));
}
$any_step = ($reparse_message ||
$reparse_change ||
$reparse_herald ||
$reparse_owners);
if ($any_step && $importing) {
throw new PhutilArgumentUsageException(
pht(
'Choosing steps with %s conflicts with flags which select '.
'specific steps.',
'--importing'));
} else if ($any_step) {
// OK.
} else if ($importing) {
// OK.
} else if (!$any_step && !$importing) {
throw new PhutilArgumentUsageException(
pht(
'Specify which steps to reparse with %s, or %s, %s, %s, or %s.',
'--importing',
'--message',
'--change',
'--herald',
'--owners'));
}
$min_timestamp = false;
if ($min_date) {
$min_timestamp = strtotime($min_date);
if (!$all_from_repo) {
throw new PhutilArgumentUsageException(
pht(
"You must use --all if you specify --min-date\n".
"e.g.\n".
" repository reparse --all TEST --owners --min-date yesterday"));
}
// previous to PHP 5.1.0 you would compare with -1, instead of false
if (false === $min_timestamp) {
throw new PhutilArgumentUsageException(
pht(
"Supplied --min-date is not valid. See help for valid examples.\n".
"Supplied value: '%s'\n",
$min_date));
}
}
if ($reparse_owners && !$force) {
$console->writeOut(
"%s\n",
pht(
'You are about to recreate the relationship entries between the '.
'commits and the packages they touch. This might delete some '.
'existing relationship entries for some old commits.'));
if (!phutil_console_confirm(pht('Are you ready to continue?'))) {
throw new PhutilArgumentUsageException(pht('Cancelled.'));
}
}
$commits = array();
if ($all_from_repo) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIdentifiers(array($all_from_repo))
->executeOne();
if (!$repository) {
throw new PhutilArgumentUsageException(
pht('Unknown repository "%s"!', $all_from_repo));
}
$query = id(new DiffusionCommitQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepository($repository);
if ($min_timestamp) {
$query->withEpochRange($min_timestamp, null);
}
if ($importing) {
$query->withImporting(true);
}
$commits = $query->execute();
if (!$commits) {
throw new PhutilArgumentUsageException(
pht(
'No commits have been discovered in the "%s" repository!',
$repository->getDisplayName()));
}
} else {
$commits = $this->loadNamedCommits($reparse_what);
}
if ($all_from_repo && !$force_local) {
$console->writeOut("%s\n", pht(
"**NOTE**: This script will queue tasks to reparse the data. Once the ".
"tasks have been queued, you need to run Taskmaster daemons to ".
"execute them.\n\n%s",
pht(
'QUEUEING TASKS (%s Commit(s)):',
phutil_count($commits))));
}
$progress = new PhutilConsoleProgressBar();
$progress->setTotal(count($commits));
$tasks = array();
foreach ($commits as $commit) {
$repository = $commit->getRepository();
if ($importing) {
$status = $commit->getImportStatus();
// Find the first missing import step and queue that up.
$reparse_message = false;
$reparse_change = false;
$reparse_owners = false;
$reparse_herald = false;
if (!($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE)) {
$reparse_message = true;
} else if (!($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE)) {
$reparse_change = true;
} else if (!($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS)) {
$reparse_owners = true;
} else if (!($status & PhabricatorRepositoryCommit::IMPORTED_HERALD)) {
$reparse_herald = true;
} else {
continue;
}
}
$classes = array();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositoryGitCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositoryGitCommitChangeParserWorker';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($reparse_message) {
$classes[] =
'PhabricatorRepositoryMercurialCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositoryMercurialCommitChangeParserWorker';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositorySvnCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositorySvnCommitChangeParserWorker';
}
break;
}
if ($reparse_herald) {
$classes[] = 'PhabricatorRepositoryCommitHeraldWorker';
}
if ($reparse_owners) {
$classes[] = 'PhabricatorRepositoryCommitOwnersWorker';
}
// NOTE: With "--importing", we queue the first unparsed step and let
// it queue the other ones normally. Without "--importing", we queue
// all the requested steps explicitly.
$spec = array(
'commitID' => $commit->getID(),
'only' => !$importing,
'forceAutoclose' => $args->getArg('force-autoclose'),
);
if ($all_from_repo && !$force_local) {
foreach ($classes as $class) {
PhabricatorWorker::scheduleTask(
$class,
$spec,
array(
'priority' => PhabricatorWorker::PRIORITY_IMPORT,
));
}
} else {
foreach ($classes as $class) {
$worker = newv($class, array($spec));
$worker->executeTask();
}
}
$progress->update(1);
}
$progress->done();
return 0;
}
}
diff --git a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php
index b5abd4003..c37bdc04f 100644
--- a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php
+++ b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php
@@ -1,127 +1,127 @@
<?php
final class PhabricatorRepositoryCommitPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'CMIT';
public function getTypeName() {
return pht('Diffusion Commit');
}
public function newObject() {
return new PhabricatorRepositoryCommit();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new DiffusionCommitQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
$unreachable = array();
foreach ($handles as $phid => $handle) {
$commit = $objects[$phid];
if ($commit->isUnreachable()) {
$unreachable[$phid] = $commit;
}
}
if ($unreachable) {
$query = id(new DiffusionCommitHintQuery())
->setViewer($query->getViewer())
->withCommits($unreachable);
$query->execute();
$hints = $query->getCommitMap();
} else {
$hints = array();
}
foreach ($handles as $phid => $handle) {
$commit = $objects[$phid];
$repository = $commit->getRepository();
$commit_identifier = $commit->getCommitIdentifier();
$name = $repository->formatCommitName($commit_identifier);
if ($commit->isUnreachable()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
// If we have a hint about this commit being rewritten, add the
// rewrite target to the handle name. This reduces the chance users
// will be caught offguard by the rewrite.
$hint = idx($hints, $phid);
if ($hint && $hint->isRewritten()) {
$new_name = $hint->getNewCommitIdentifier();
$new_name = $repository->formatCommitName($new_name);
$name = pht("%s \xE2\x99\xBB %s", $name, $new_name);
}
}
$summary = $commit->getSummary();
if (strlen($summary)) {
$full_name = $name.': '.$summary;
} else {
$full_name = $name;
}
$handle->setName($name);
$handle->setFullName($full_name);
$handle->setURI($commit->getURI());
$handle->setTimestamp($commit->getEpoch());
- $status = $commit->getAuditStatus();
- $icon = PhabricatorAuditCommitStatusConstants::getStatusIcon($status);
- $color = PhabricatorAuditCommitStatusConstants::getStatusColor($status);
- $name = PhabricatorAuditCommitStatusConstants::getStatusName($status);
+ $status = $commit->getAuditStatusObject();
+ $icon = $status->getIcon();
+ $color = $status->getColor();
+ $name = $status->getName();
$handle
->setStateIcon($icon)
->setStateColor($color)
->setStateName($name);
}
}
public static function getCommitObjectNamePattern() {
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
return
'(?:r[A-Z]+:?|R[0-9]+:)[1-9]\d*'.
'|'.
'(?:r[A-Z]+:?|R[0-9]+:)[a-f0-9]{'.$min_qualified.',40}'.
'|'.
'[a-f0-9]{'.$min_unqualified.',40}';
}
public function canLoadNamedObject($name) {
$pattern = self::getCommitObjectNamePattern();
return preg_match('(^'.$pattern.'$)', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$query = id(new DiffusionCommitQuery())
->setViewer($query->getViewer())
->withIdentifiers($names);
$query->execute();
return $query->getIdentifierMap();
}
}
diff --git a/src/applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php
new file mode 100644
index 000000000..5d9b06e0a
--- /dev/null
+++ b/src/applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php
@@ -0,0 +1,46 @@
+<?php
+
+final class PhabricatorRepositoryIdentityPHIDType
+ extends PhabricatorPHIDType {
+
+ const TYPECONST = 'RIDT';
+
+ public function getTypeName() {
+ return pht('Repository Identity');
+ }
+
+ public function newObject() {
+ return new PhabricatorRepositoryIdentity();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorDiffusionApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorRepositoryIdentityQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $identity = $objects[$phid];
+
+ $id = $identity->getID();
+ $name = $identity->getIdentityNameRaw();
+
+ $handle->setObjectName(pht('Identity %d', $id));
+ $handle->setName($name);
+ $handle->setURI($identity->getURI());
+ $handle->setIcon('fa-user');
+ }
+ }
+
+}
diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php
new file mode 100644
index 000000000..ef038f045
--- /dev/null
+++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php
@@ -0,0 +1,154 @@
+<?php
+
+final class PhabricatorRepositoryIdentityQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $identityNames;
+ private $emailAddress;
+ private $assigneePHIDs;
+ private $identityNameLike;
+ private $hasEffectivePHID;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withIdentityNames(array $names) {
+ $this->identityNames = $names;
+ return $this;
+ }
+
+ public function withIdentityNameLike($name_like) {
+ $this->identityNameLike = $name_like;
+ return $this;
+ }
+
+ public function withEmailAddress($address) {
+ $this->emailAddress = $address;
+ return $this;
+ }
+
+ public function withAssigneePHIDs(array $assignees) {
+ $this->assigneePHIDs = $assignees;
+ return $this;
+ }
+
+ public function withHasEffectivePHID($has_effective_phid) {
+ $this->hasEffectivePHID = $has_effective_phid;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorRepositoryIdentity();
+ }
+
+ protected function getPrimaryTableAlias() {
+ return 'repository_identity';
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->assigneePHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.currentEffectiveUserPHID IN (%Ls)',
+ $this->assigneePHIDs);
+ }
+
+ if ($this->hasEffectivePHID !== null) {
+ if ($this->hasEffectivePHID) {
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.currentEffectiveUserPHID IS NOT NULL');
+ } else {
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.currentEffectiveUserPHID IS NULL');
+ }
+ }
+
+ if ($this->identityNames !== null) {
+ $name_hashes = array();
+ foreach ($this->identityNames as $name) {
+ $name_hashes[] = PhabricatorHash::digestForIndex($name);
+ }
+
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.identityNameHash IN (%Ls)',
+ $name_hashes);
+ }
+
+ if ($this->emailAddress !== null) {
+ $identity_style = "<{$this->emailAddress}>";
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.identityNameRaw LIKE %<',
+ $identity_style);
+ }
+
+ if ($this->identityNameLike != null) {
+ $where[] = qsprintf(
+ $conn,
+ 'repository_identity.identityNameRaw LIKE %~',
+ $this->identityNameLike);
+ }
+
+ return $where;
+ }
+
+ protected function didFilterPage(array $identities) {
+ $user_ids = array_filter(
+ mpull($identities, 'getCurrentEffectiveUserPHID', 'getID'));
+ if (!$user_ids) {
+ return $identities;
+ }
+
+ $users = id(new PhabricatorPeopleQuery())
+ ->withPHIDs($user_ids)
+ ->setViewer($this->getViewer())
+ ->execute();
+ $users = mpull($users, null, 'getPHID');
+
+ foreach ($identities as $identity) {
+ if ($identity->hasEffectiveUser()) {
+ $user = idx($users, $identity->getCurrentEffectiveUserPHID());
+ $identity->attachEffectiveUser($user);
+ }
+ }
+
+ return $identities;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorDiffusionApplication';
+ }
+
+}
diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php
new file mode 100644
index 000000000..f62a8610f
--- /dev/null
+++ b/src/applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorRepositoryIdentityTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorRepositoryIdentityTransaction();
+ }
+
+}
diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php
index cb097fae2..d4734f61e 100644
--- a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php
@@ -1,159 +1,199 @@
<?php
final class PhabricatorRepositoryPushLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $repositoryPHIDs;
private $pusherPHIDs;
private $refTypes;
private $newRefs;
private $pushEventPHIDs;
private $epochMin;
private $epochMax;
+ private $blockingHeraldRulePHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function withPusherPHIDs(array $pusher_phids) {
$this->pusherPHIDs = $pusher_phids;
return $this;
}
public function withRefTypes(array $ref_types) {
$this->refTypes = $ref_types;
return $this;
}
public function withNewRefs(array $new_refs) {
$this->newRefs = $new_refs;
return $this;
}
public function withPushEventPHIDs(array $phids) {
$this->pushEventPHIDs = $phids;
return $this;
}
public function withEpochBetween($min, $max) {
$this->epochMin = $min;
$this->epochMax = $max;
return $this;
}
+ public function withBlockingHeraldRulePHIDs(array $phids) {
+ $this->blockingHeraldRulePHIDs = $phids;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorRepositoryPushLog();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $logs) {
$event_phids = mpull($logs, 'getPushEventPHID');
$events = id(new PhabricatorObjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($event_phids)
->execute();
$events = mpull($events, null, 'getPHID');
foreach ($logs as $key => $log) {
$event = idx($events, $log->getPushEventPHID());
if (!$event) {
unset($logs[$key]);
continue;
}
$log->attachPushEvent($event);
}
return $logs;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
- 'id IN (%Ld)',
+ 'log.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
- 'phid IN (%Ls)',
+ 'log.phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
- 'repositoryPHID IN (%Ls)',
+ 'log.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->pusherPHIDs !== null) {
$where[] = qsprintf(
$conn,
- 'pusherPHID in (%Ls)',
+ 'log.pusherPHID in (%Ls)',
$this->pusherPHIDs);
}
if ($this->pushEventPHIDs !== null) {
$where[] = qsprintf(
$conn,
- 'pushEventPHID in (%Ls)',
+ 'log.pushEventPHID in (%Ls)',
$this->pushEventPHIDs);
}
if ($this->refTypes !== null) {
$where[] = qsprintf(
$conn,
- 'refType IN (%Ls)',
+ 'log.refType IN (%Ls)',
$this->refTypes);
}
if ($this->newRefs !== null) {
$where[] = qsprintf(
$conn,
- 'refNew IN (%Ls)',
+ 'log.refNew IN (%Ls)',
$this->newRefs);
}
if ($this->epochMin !== null) {
$where[] = qsprintf(
$conn,
- 'epoch >= %d',
+ 'log.epoch >= %d',
$this->epochMin);
}
if ($this->epochMax !== null) {
$where[] = qsprintf(
$conn,
- 'epoch <= %d',
+ 'log.epoch <= %d',
$this->epochMax);
}
+ if ($this->blockingHeraldRulePHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ '(event.rejectCode = %d AND event.rejectDetails IN (%Ls))',
+ PhabricatorRepositoryPushLog::REJECT_HERALD,
+ $this->blockingHeraldRulePHIDs);
+ }
+
return $where;
}
+ protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
+ $joins = parent::buildJoinClauseParts($conn);
+
+ if ($this->shouldJoinPushEventTable()) {
+ $joins[] = qsprintf(
+ $conn,
+ 'JOIN %T event ON event.phid = log.pushEventPHID',
+ id(new PhabricatorRepositoryPushEvent())->getTableName());
+ }
+
+ return $joins;
+ }
+
+ private function shouldJoinPushEventTable() {
+ if ($this->blockingHeraldRulePHIDs !== null) {
+ return true;
+ }
+
+ return false;
+ }
+
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
+ protected function getPrimaryTableAlias() {
+ return 'log';
+ }
+
+
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php
index 7b626fff3..87b2e4474 100644
--- a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php
@@ -1,255 +1,269 @@
<?php
final class PhabricatorRepositoryPushLogSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Push Logs');
}
public function getApplicationClassName() {
return 'PhabricatorDiffusionApplication';
}
public function newQuery() {
return new PhabricatorRepositoryPushLogQuery();
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['repositoryPHIDs']) {
$query->withRepositoryPHIDs($map['repositoryPHIDs']);
}
if ($map['pusherPHIDs']) {
$query->withPusherPHIDs($map['pusherPHIDs']);
}
if ($map['createdStart'] || $map['createdEnd']) {
$query->withEpochBetween(
$map['createdStart'],
$map['createdEnd']);
}
+ if ($map['blockingHeraldRulePHIDs']) {
+ $query->withBlockingHeraldRulePHIDs($map['blockingHeraldRulePHIDs']);
+ }
+
return $query;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setDatasource(new DiffusionRepositoryDatasource())
->setKey('repositoryPHIDs')
->setAliases(array('repository', 'repositories', 'repositoryPHID'))
->setLabel(pht('Repositories'))
->setDescription(
- pht('Search for pull logs for specific repositories.')),
+ pht('Search for push logs for specific repositories.')),
id(new PhabricatorUsersSearchField())
->setKey('pusherPHIDs')
->setAliases(array('pusher', 'pushers', 'pusherPHID'))
->setLabel(pht('Pushers'))
->setDescription(
- pht('Search for pull logs by specific users.')),
+ pht('Search for push logs by specific users.')),
+ id(new PhabricatorSearchDatasourceField())
+ ->setDatasource(new HeraldRuleDatasource())
+ ->setKey('blockingHeraldRulePHIDs')
+ ->setLabel(pht('Blocked By'))
+ ->setDescription(
+ pht('Search for pushes blocked by particular Herald rules.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created After'))
->setKey('createdStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created Before'))
->setKey('createdEnd'),
);
}
protected function getURI($path) {
return '/diffusion/pushlog/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Push Logs'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $logs,
PhabricatorSavedQuery $query,
array $handles) {
$table = id(new DiffusionPushLogListView())
->setViewer($this->requireViewer())
->setLogs($logs);
return id(new PhabricatorApplicationSearchResultView())
->setTable($table);
}
protected function newExportFields() {
$viewer = $this->requireViewer();
$fields = array(
id(new PhabricatorIDExportField())
->setKey('pushID')
->setLabel(pht('Push ID')),
id(new PhabricatorStringExportField())
->setKey('unique')
->setLabel(pht('Unique')),
id(new PhabricatorStringExportField())
->setKey('protocol')
->setLabel(pht('Protocol')),
id(new PhabricatorPHIDExportField())
->setKey('repositoryPHID')
->setLabel(pht('Repository PHID')),
id(new PhabricatorStringExportField())
->setKey('repository')
->setLabel(pht('Repository')),
id(new PhabricatorPHIDExportField())
->setKey('pusherPHID')
->setLabel(pht('Pusher PHID')),
id(new PhabricatorStringExportField())
->setKey('pusher')
->setLabel(pht('Pusher')),
id(new PhabricatorPHIDExportField())
->setKey('devicePHID')
->setLabel(pht('Device PHID')),
id(new PhabricatorStringExportField())
->setKey('device')
->setLabel(pht('Device')),
id(new PhabricatorStringExportField())
->setKey('type')
->setLabel(pht('Ref Type')),
id(new PhabricatorStringExportField())
->setKey('name')
->setLabel(pht('Ref Name')),
id(new PhabricatorStringExportField())
->setKey('old')
->setLabel(pht('Ref Old')),
id(new PhabricatorStringExportField())
->setKey('new')
->setLabel(pht('Ref New')),
id(new PhabricatorIntExportField())
->setKey('flags')
->setLabel(pht('Flags')),
id(new PhabricatorStringListExportField())
->setKey('flagNames')
->setLabel(pht('Flag Names')),
id(new PhabricatorIntExportField())
->setKey('result')
->setLabel(pht('Result')),
id(new PhabricatorStringExportField())
->setKey('resultName')
->setLabel(pht('Result Name')),
+ id(new PhabricatorStringExportField())
+ ->setKey('resultDetails')
+ ->setLabel(pht('Result Details')),
id(new PhabricatorIntExportField())
->setKey('writeWait')
->setLabel(pht('Write Wait (us)')),
id(new PhabricatorIntExportField())
->setKey('readWait')
->setLabel(pht('Read Wait (us)')),
id(new PhabricatorIntExportField())
->setKey('hostWait')
->setLabel(pht('Host Wait (us)')),
);
if ($viewer->getIsAdmin()) {
$fields[] = id(new PhabricatorStringExportField())
->setKey('remoteAddress')
->setLabel(pht('Remote Address'));
}
return $fields;
}
protected function newExportData(array $logs) {
$viewer = $this->requireViewer();
$phids = array();
foreach ($logs as $log) {
$phids[] = $log->getPusherPHID();
$phids[] = $log->getDevicePHID();
$phids[] = $log->getPushEvent()->getRepositoryPHID();
}
$handles = $viewer->loadHandles($phids);
$flag_map = PhabricatorRepositoryPushLog::getFlagDisplayNames();
$reject_map = PhabricatorRepositoryPushLog::getRejectCodeDisplayNames();
$export = array();
foreach ($logs as $log) {
$event = $log->getPushEvent();
$repository_phid = $event->getRepositoryPHID();
if ($repository_phid) {
$repository_name = $handles[$repository_phid]->getName();
} else {
$repository_name = null;
}
$pusher_phid = $log->getPusherPHID();
if ($pusher_phid) {
$pusher_name = $handles[$pusher_phid]->getName();
} else {
$pusher_name = null;
}
$device_phid = $log->getDevicePHID();
if ($device_phid) {
$device_name = $handles[$device_phid]->getName();
} else {
$device_name = null;
}
$flags = $log->getChangeFlags();
$flag_names = array();
foreach ($flag_map as $flag_key => $flag_name) {
if (($flags & $flag_key) === $flag_key) {
$flag_names[] = $flag_name;
}
}
$result = $event->getRejectCode();
$result_name = idx($reject_map, $result, pht('Unknown ("%s")', $result));
$map = array(
'pushID' => $event->getID(),
'unique' => $event->getRequestIdentifier(),
'protocol' => $event->getRemoteProtocol(),
'repositoryPHID' => $repository_phid,
'repository' => $repository_name,
'pusherPHID' => $pusher_phid,
'pusher' => $pusher_name,
'devicePHID' => $device_phid,
'device' => $device_name,
'type' => $log->getRefType(),
'name' => $log->getRefName(),
'old' => $log->getRefOld(),
'new' => $log->getRefNew(),
'flags' => $flags,
'flagNames' => $flag_names,
'result' => $result,
'resultName' => $result_name,
+ 'resultDetails' => $event->getRejectDetails(),
'writeWait' => $event->getWriteWait(),
'readWait' => $event->getReadWait(),
'hostWait' => $event->getHostWait(),
);
if ($viewer->getIsAdmin()) {
$map['remoteAddress'] = $event->getRemoteAddress();
}
$export[] = $map;
}
return $export;
}
}
diff --git a/src/applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php b/src/applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php
new file mode 100644
index 000000000..d922792bb
--- /dev/null
+++ b/src/applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorRepositoryIdentityFerretEngine
+ extends PhabricatorFerretEngine {
+
+ public function getApplicationName() {
+ return 'repository';
+ }
+
+ public function getScopeName() {
+ return 'identity';
+ }
+
+ public function newSearchEngine() {
+ return new DiffusionRepositoryIdentitySearchEngine();
+ }
+
+}
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index 17efc1344..6e52a25c8 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,2707 +1,2707 @@
<?php
/**
* @task uri Repository URI Management
* @task autoclose Autoclose
* @task sync Cluster Synchronization
*/
final class PhabricatorRepository extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorMarkupInterface,
PhabricatorDestructibleInterface,
PhabricatorDestructibleCodexInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
/**
* Shortest hash we'll recognize in raw "a829f32" form.
*/
const MINIMUM_UNQUALIFIED_HASH = 7;
/**
* Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
*/
const MINIMUM_QUALIFIED_HASH = 5;
/**
* Minimum number of commits to an empty repository to trigger "import" mode.
*/
const IMPORT_THRESHOLD = 7;
const TABLE_PATH = 'repository_path';
const TABLE_PATHCHANGE = 'repository_pathchange';
const TABLE_FILESYSTEM = 'repository_filesystem';
const TABLE_SUMMARY = 'repository_summary';
const TABLE_LINTMESSAGE = 'repository_lintmessage';
const TABLE_PARENTS = 'repository_parents';
const TABLE_COVERAGE = 'repository_coverage';
const BECAUSE_REPOSITORY_IMPORTING = 'auto/importing';
const BECAUSE_AUTOCLOSE_DISABLED = 'auto/disabled';
const BECAUSE_NOT_ON_AUTOCLOSE_BRANCH = 'auto/nobranch';
const BECAUSE_BRANCH_UNTRACKED = 'auto/notrack';
const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
const BECAUSE_AUTOCLOSE_FORCED = 'auto/forced';
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
protected $name;
protected $callsign;
protected $repositorySlug;
protected $uuid;
protected $viewPolicy;
protected $editPolicy;
protected $pushPolicy;
protected $profileImagePHID;
protected $versionControlSystem;
protected $details = array();
protected $credentialPHID;
protected $almanacServicePHID;
protected $spacePHID;
protected $localPath;
private $commitCount = self::ATTACHABLE;
private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE;
private $uris = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDiffusionApplication'))
->executeOne();
$view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
$push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
$repository = id(new PhabricatorRepository())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setPushPolicy($push_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
// Put the repository in "Importing" mode until we finish
// parsing it.
$repository->setDetail('importing', true);
return $repository;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'callsign' => 'sort32?',
'repositorySlug' => 'sort64?',
'versionControlSystem' => 'text32',
'uuid' => 'text64?',
'pushPolicy' => 'policy',
'credentialPHID' => 'phid?',
'almanacServicePHID' => 'phid?',
'localPath' => 'text128?',
'profileImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'callsign' => array(
'columns' => array('callsign'),
'unique' => true,
),
'key_name' => array(
'columns' => array('name(128)'),
),
'key_vcs' => array(
'columns' => array('versionControlSystem'),
),
'key_slug' => array(
'columns' => array('repositorySlug'),
'unique' => true,
),
'key_local' => array(
'columns' => array('localPath'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
}
public static function getStatusMap() {
return array(
self::STATUS_ACTIVE => array(
'name' => pht('Active'),
'isTracked' => 1,
),
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'isTracked' => 0,
),
);
}
public static function getStatusNameMap() {
return ipull(self::getStatusMap(), 'name');
}
public function getStatus() {
if ($this->isTracked()) {
return self::STATUS_ACTIVE;
} else {
return self::STATUS_INACTIVE;
}
}
public function toDictionary() {
return array(
'id' => $this->getID(),
'name' => $this->getName(),
'phid' => $this->getPHID(),
'callsign' => $this->getCallsign(),
'monogram' => $this->getMonogram(),
'vcs' => $this->getVersionControlSystem(),
'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
'remoteURI' => (string)$this->getRemoteURI(),
'description' => $this->getDetail('description'),
'isActive' => $this->isTracked(),
'isHosted' => $this->isHosted(),
'isImporting' => $this->isImporting(),
'encoding' => $this->getDefaultTextEncoding(),
'staging' => array(
'supported' => $this->supportsStaging(),
'prefix' => 'phabricator',
'uri' => $this->getStagingURI(),
),
);
}
public function getDefaultTextEncoding() {
return $this->getDetail('encoding', 'UTF-8');
}
public function getMonogram() {
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "r{$callsign}";
}
$id = $this->getID();
return "R{$id}";
}
public function getDisplayName() {
$slug = $this->getRepositorySlug();
if (strlen($slug)) {
return $slug;
}
return $this->getMonogram();
}
public function getAllMonograms() {
$monograms = array();
$monograms[] = 'R'.$this->getID();
$callsign = $this->getCallsign();
if (strlen($callsign)) {
$monograms[] = 'r'.$callsign;
}
return $monograms;
}
public function setLocalPath($path) {
// Convert any extra slashes ("//") in the path to a single slash ("/").
$path = preg_replace('(//+)', '/', $path);
return parent::setLocalPath($path);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function 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 getSubversionBaseURI($commit = null) {
$subpath = $this->getDetail('svn-subpath');
if (!strlen($subpath)) {
$subpath = null;
}
return $this->getSubversionPathURI($subpath, $commit);
}
public function getSubversionPathURI($path = null, $commit = null) {
$vcs = $this->getVersionControlSystem();
if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
throw new Exception(pht('Not a subversion repository!'));
}
if ($this->isHosted()) {
$uri = 'file://'.$this->getLocalPath();
} else {
$uri = $this->getDetail('remote-uri');
}
$uri = rtrim($uri, '/');
if (strlen($path)) {
$path = rawurlencode($path);
$path = str_replace('%2F', '/', $path);
$uri = $uri.'/'.ltrim($path, '/');
}
if ($path !== null || $commit !== null) {
$uri .= '@';
}
if ($commit !== null) {
$uri .= $commit;
}
return $uri;
}
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/**
* Get the name of the directory this repository should clone or checkout
* into. For example, if the repository name is "Example Repository", a
* reasonable name might be "example-repository". This is used to help users
* get reasonable results when cloning repositories, since they generally do
* not want to clone into directories called "X/" or "Example Repository/".
*
* @return string
*/
public function getCloneName() {
$name = $this->getRepositorySlug();
// Make some reasonable effort to produce reasonable default directory
// names from repository names.
if (!strlen($name)) {
$name = $this->getName();
$name = phutil_utf8_strtolower($name);
- $name = preg_replace('@[/ -:<>]+@', '-', $name);
+ $name = preg_replace('@[ -/:->]+@', '-', $name);
$name = trim($name, '-');
if (!strlen($name)) {
$name = $this->getCallsign();
}
}
return $name;
}
public static function isValidRepositorySlug($slug) {
try {
self::assertValidRepositorySlug($slug);
return true;
} catch (Exception $ex) {
return false;
}
}
public static function assertValidRepositorySlug($slug) {
if (!strlen($slug)) {
throw new Exception(
pht(
'The empty string is not a valid repository short name. '.
'Repository short names must be at least one character long.'));
}
if (strlen($slug) > 64) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not be longer than 64 characters.',
$slug));
}
if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may only contain letters, numbers, periods, hyphens '.
'and underscores.',
$slug));
}
if (!preg_match('/^[a-zA-Z0-9]/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must begin with a letter or number.',
$slug));
}
if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must end with a letter or number.',
$slug));
}
if (preg_match('/__|--|\\.\\./', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not contain multiple consecutive underscores, '.
'hyphens, or periods.',
$slug));
}
if (preg_match('/^[A-Z]+\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may not contain only uppercase letters.',
$slug));
}
if (preg_match('/^\d+\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may not contain only numbers.',
$slug));
}
if (preg_match('/\\.git/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not end in ".git". This suffix will be added '.
'automatically in appropriate contexts.',
$slug));
}
}
public static function assertValidCallsign($callsign) {
if (!strlen($callsign)) {
throw new Exception(
pht(
'A repository callsign must be at least one character long.'));
}
if (strlen($callsign) > 32) {
throw new Exception(
pht(
'The callsign "%s" is not a valid repository callsign. Callsigns '.
'must be no more than 32 bytes long.',
$callsign));
}
if (!preg_match('/^[A-Z]+\z/', $callsign)) {
throw new Exception(
pht(
'The callsign "%s" is not a valid repository callsign. Callsigns '.
'may only contain UPPERCASE letters.',
$callsign));
}
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/* -( Remote Command Execution )------------------------------------------- */
public function execRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolve();
}
public function execxRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolvex();
}
public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args);
}
public function passthruRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandPassthru($args)->execute();
}
private function newRemoteCommandFuture(array $argv) {
return $this->newRemoteCommandEngine($argv)
->newFuture();
}
private function newRemoteCommandPassthru(array $argv) {
return $this->newRemoteCommandEngine($argv)
->setPassthru(true)
->newFuture();
}
private function newRemoteCommandEngine(array $argv) {
return DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setCredentialPHID($this->getCredentialPHID())
->setURI($this->getRemoteURIObject());
}
/* -( Local Command Execution )-------------------------------------------- */
public function execLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolve();
}
public function execxLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolvex();
}
public function getLocalCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args);
}
public function passthruLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandPassthru($args)->execute();
}
private function newLocalCommandFuture(array $argv) {
$this->assertLocalExists();
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->newFuture();
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
private function newLocalCommandPassthru(array $argv) {
$this->assertLocalExists();
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setPassthru(true)
->newFuture();
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
public function getURI() {
$short_name = $this->getRepositorySlug();
if (strlen($short_name)) {
return "/source/{$short_name}/";
}
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/diffusion/{$callsign}/";
}
$id = $this->getID();
return "/diffusion/{$id}/";
}
public function getPathURI($path) {
return $this->getURI().ltrim($path, '/');
}
public function getCommitURI($identifier) {
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/r{$callsign}{$identifier}";
}
$id = $this->getID();
return "/R{$id}:{$identifier}";
}
public static function parseRepositoryServicePath($request_path, $vcs) {
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$patterns = array(
'(^'.
'(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
'(?P<path>.*)'.
'\z)',
);
$identifier = null;
foreach ($patterns as $pattern) {
$matches = null;
if (!preg_match($pattern, $request_path, $matches)) {
continue;
}
$identifier = $matches['identifier'];
if ($is_git) {
$identifier = preg_replace('/\\.git\z/', '', $identifier);
}
$base = $matches['base'];
$path = $matches['path'];
break;
}
if ($identifier === null) {
return null;
}
return array(
'identifier' => $identifier,
'base' => $base,
'path' => $path,
);
}
public function getCanonicalPath($request_path) {
$standard_pattern =
'(^'.
'(?P<prefix>/(?:diffusion|source)/)'.
'(?P<identifier>[^/]+)'.
'(?P<suffix>(?:/.*)?)'.
'\z)';
$matches = null;
if (preg_match($standard_pattern, $request_path, $matches)) {
$suffix = $matches['suffix'];
return $this->getPathURI($suffix);
}
$commit_pattern =
'(^'.
'(?P<prefix>/)'.
'(?P<monogram>'.
'(?:'.
'r(?P<repositoryCallsign>[A-Z]+)'.
'|'.
'R(?P<repositoryID>[1-9]\d*):'.
')'.
'(?P<commit>[a-f0-9]+)'.
')'.
'\z)';
$matches = null;
if (preg_match($commit_pattern, $request_path, $matches)) {
$commit = $matches['commit'];
return $this->getCommitURI($commit);
}
return null;
}
public function generateURI(array $params) {
$req_branch = false;
$req_commit = false;
$action = idx($params, 'action');
switch ($action) {
case 'history':
case 'graph':
case 'clone':
case 'jobs': // c4science custo
case 'blame':
case 'browse':
case 'document':
case 'change':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
case 'compare':
break;
case 'branch':
// NOTE: This does not actually require a branch, and won't have one
// in Subversion. Possibly this should be more clear.
break;
case 'commit':
case 'rendering-ref':
$req_commit = true;
break;
default:
throw new Exception(
pht(
'Action "%s" is not a valid repository URI action.',
$action));
}
$path = idx($params, 'path');
$branch = idx($params, 'branch');
$commit = idx($params, 'commit');
$line = idx($params, 'line');
$head = idx($params, 'head');
$against = idx($params, 'against');
if ($req_commit && !strlen($commit)) {
throw new Exception(
pht(
'Diffusion URI action "%s" requires commit!',
$action));
}
if ($req_branch && !strlen($branch)) {
throw new Exception(
pht(
'Diffusion URI action "%s" requires branch!',
$action));
}
if ($action === 'commit') {
return $this->getCommitURI($commit);
}
if (strlen($path)) {
$path = ltrim($path, '/');
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
$path = phutil_escape_uri($path);
}
$raw_branch = $branch;
if (strlen($branch)) {
$branch = phutil_escape_uri_path_component($branch);
$path = "{$branch}/{$path}";
}
$raw_commit = $commit;
if (strlen($commit)) {
$commit = str_replace('$', '$$', $commit);
$commit = ';'.phutil_escape_uri($commit);
}
if (strlen($line)) {
$line = '$'.phutil_escape_uri($line);
}
$query = array();
switch ($action) {
case 'change':
case 'history':
case 'graph':
case 'jobs': // c4science custo
case 'blame':
case 'browse':
case 'document':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
$uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
break;
case 'compare':
$uri = $this->getPathURI("/{$action}/");
if (strlen($head)) {
$query['head'] = $head;
} else if (strlen($raw_commit)) {
$query['commit'] = $raw_commit;
} else if (strlen($raw_branch)) {
$query['head'] = $raw_branch;
}
if (strlen($against)) {
$query['against'] = $against;
}
break;
case 'branch':
if (strlen($path)) {
$uri = $this->getPathURI("/repository/{$path}");
} else {
$uri = $this->getPathURI('/');
}
break;
case 'external':
$commit = ltrim($commit, ';');
$uri = "/diffusion/external/{$commit}/";
break;
case 'rendering-ref':
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri = rawurldecode("{$path}{$commit}");
break;
case 'clone':
$uri = $this->getPathURI("/{$action}/");
break;
}
if ($action == 'rendering-ref') {
return $uri;
}
$uri = new PhutilURI($uri);
if (isset($params['lint'])) {
$params['params'] = idx($params, 'params', array()) + array(
'lint' => $params['lint'],
);
}
$query = idx($params, 'params', array()) + $query;
if ($query) {
$uri->setQueryParams($query);
}
return $uri;
}
public function updateURIIndex() {
$indexes = array();
$uris = $this->getURIs();
foreach ($uris as $uri) {
if ($uri->getIsDisabled()) {
continue;
}
$indexes[] = $uri->getNormalizedURI();
}
PhabricatorRepositoryURIIndex::updateRepositoryURIs(
$this->getPHID(),
$indexes);
return $this;
}
public function isTracked() {
$status = $this->getDetail('tracking-enabled');
$map = self::getStatusMap();
$spec = idx($map, $status);
if (!$spec) {
if ($status) {
$status = self::STATUS_ACTIVE;
} else {
$status = self::STATUS_INACTIVE;
}
$spec = idx($map, $status);
}
return (bool)idx($spec, 'isTracked', false);
}
public function getDefaultBranch() {
$default = $this->getDetail('default-branch');
if (strlen($default)) {
return $default;
}
$default_branches = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
);
return idx($default_branches, $this->getVersionControlSystem());
}
public function getDefaultArcanistBranch() {
return coalesce($this->getDefaultBranch(), 'svn');
}
private function isBranchInFilter($branch, $filter_key) {
$vcs = $this->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$use_filter = ($is_git);
if (!$use_filter) {
// If this VCS doesn't use filters, pass everything through.
return true;
}
$filter = $this->getDetail($filter_key, array());
// If there's no filter set, let everything through.
if (!$filter) {
return true;
}
// If this branch isn't literally named `regexp(...)`, and it's in the
// filter list, let it through.
if (isset($filter[$branch])) {
if (self::extractBranchRegexp($branch) === null) {
return true;
}
}
// If the branch matches a regexp, let it through.
foreach ($filter as $pattern => $ignored) {
$regexp = self::extractBranchRegexp($pattern);
if ($regexp !== null) {
if (preg_match($regexp, $branch)) {
return true;
}
}
}
// Nothing matched, so filter this branch out.
return false;
}
public static function extractBranchRegexp($pattern) {
$matches = null;
if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
return $matches[1];
}
return null;
}
public function shouldTrackRef(DiffusionRepositoryRef $ref) {
// At least for now, don't track the staging area tags.
if ($ref->isTag()) {
if (preg_match('(^phabricator/)', $ref->getShortName())) {
return false;
}
}
if (!$ref->isBranch()) {
return true;
}
return $this->shouldTrackBranch($ref->getShortName());
}
public function shouldTrackBranch($branch) {
return $this->isBranchInFilter($branch, 'branch-filter');
}
public function formatCommitName($commit_identifier, $local = false) {
$vcs = $this->getVersionControlSystem();
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$is_git = ($vcs == $type_git);
$is_hg = ($vcs == $type_hg);
if ($is_git || $is_hg) {
$name = substr($commit_identifier, 0, 12);
$need_scope = false;
} else {
$name = $commit_identifier;
$need_scope = true;
}
if (!$local) {
$need_scope = true;
}
if ($need_scope) {
$callsign = $this->getCallsign();
if ($callsign) {
$scope = "r{$callsign}";
} else {
$id = $this->getID();
$scope = "R{$id}:";
}
$name = $scope.$name;
}
return $name;
}
public function isImporting() {
return (bool)$this->getDetail('importing', false);
}
public function isNewlyInitialized() {
return (bool)$this->getDetail('newly-initialized', false);
}
public function loadImportProgress() {
$progress = queryfx_all(
$this->establishConnection('r'),
'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
GROUP BY importStatus',
id(new PhabricatorRepositoryCommit())->getTableName(),
$this->getID());
$done = 0;
$total = 0;
foreach ($progress as $row) {
$total += $row['N'] * 4;
$status = $row['importStatus'];
if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_HERALD) {
$done += $row['N'];
}
}
if ($total) {
$ratio = ($done / $total);
} else {
$ratio = 0;
}
// Cap this at "99.99%", because it's confusing to users when the actual
// fraction is "99.996%" and it rounds up to "100.00%".
if ($ratio > 0.9999) {
$ratio = 0.9999;
}
return $ratio;
}
/**
* Should this repository publish feed, notifications, audits, and email?
*
* We do not publish information about repositories during initial import,
* or if the repository has been set not to publish.
*/
public function shouldPublish() {
if ($this->isImporting()) {
return false;
}
if ($this->getDetail('herald-disabled')) {
return false;
}
return true;
}
/* -( Autoclose )---------------------------------------------------------- */
public function shouldAutocloseRef(DiffusionRepositoryRef $ref) {
if (!$ref->isBranch()) {
return false;
}
return $this->shouldAutocloseBranch($ref->getShortName());
}
/**
* Determine if autoclose is active for a branch.
*
* For more details about why, use @{method:shouldSkipAutocloseBranch}.
*
* @param string Branch name to check.
* @return bool True if autoclose is active for the branch.
* @task autoclose
*/
public function shouldAutocloseBranch($branch) {
return ($this->shouldSkipAutocloseBranch($branch) === null);
}
/**
* Determine if autoclose is active for a commit.
*
* For more details about why, use @{method:shouldSkipAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return bool True if autoclose is active for the commit.
* @task autoclose
*/
public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) {
return ($this->shouldSkipAutocloseCommit($commit) === null);
}
/**
* Determine why autoclose should be skipped for a branch.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseBranch}.
*
* @param string Branch name to check.
* @return const|null Constant identifying reason to skip this branch, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseBranch($branch) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
if (!$this->shouldTrackBranch($branch)) {
return self::BECAUSE_BRANCH_UNTRACKED;
}
if (!$this->isBranchInFilter($branch, 'close-commits-filter')) {
return self::BECAUSE_BRANCH_NOT_AUTOCLOSE;
}
return null;
}
/**
* Determine why autoclose should be skipped for a commit.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return const|null Constant identifying reason to skip this commit, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseCommit(
PhabricatorRepositoryCommit $commit) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return null;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
if (!$commit->isPartiallyImported($closeable_flag)) {
return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH;
}
return null;
}
/**
* Determine why all autoclose operations should be skipped for this
* repository.
*
* @return const|null Constant identifying reason to skip all autoclose
* operations, or null if autoclose operations are not blocked at the
* repository level.
* @task autoclose
*/
private function shouldSkipAllAutoclose() {
if ($this->isImporting()) {
return self::BECAUSE_REPOSITORY_IMPORTING;
}
if ($this->getDetail('disable-autoclose', false)) {
return self::BECAUSE_AUTOCLOSE_DISABLED;
}
return null;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public function getRemoteURI() {
return (string)$this->getRemoteURIObject();
}
/**
* Get the remote URI for this repository, including credentials if they're
* used by this repository.
*
* @return PhutilOpaqueEnvelope URI, possibly including credentials.
* @task uri
*/
public function getRemoteURIEnvelope() {
$uri = $this->getRemoteURIObject();
$remote_protocol = $this->getRemoteProtocol();
if ($remote_protocol == 'http' || $remote_protocol == 'https') {
// For SVN, we use `--username` and `--password` flags separately, so
// don't add any credentials here.
if (!$this->isSVN()) {
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$uri->setUser($key->getUsernameEnvelope()->openEnvelope());
$uri->setPass($key->getPasswordEnvelope()->openEnvelope());
}
}
}
return new PhutilOpaqueEnvelope((string)$uri);
}
/**
* Get the clone (or checkout) URI for this repository, without authentication
* information.
*
* @return string Repository URI.
* @task uri
*/
public function getPublicCloneURI() {
return (string)$this->getCloneURIObject();
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public function getRemoteProtocol() {
$uri = $this->getRemoteURIObject();
return $uri->getProtocol();
}
/**
* Get a parsed object representation of the repository's remote URI..
*
* @return wild A @{class@libphutil:PhutilURI}.
* @task uri
*/
public function getRemoteURIObject() {
$raw_uri = $this->getDetail('remote-uri');
if (!strlen($raw_uri)) {
return new PhutilURI('');
}
if (!strncmp($raw_uri, '/', 1)) {
return new PhutilURI('file://'.$raw_uri);
}
return new PhutilURI($raw_uri);
}
/**
* Get the "best" clone/checkout URI for this repository, on any protocol.
*/
public function getCloneURIObject() {
if (!$this->isHosted()) {
if ($this->isSVN()) {
// Make sure we pick up the "Import Only" path for Subversion, so
// the user clones the repository starting at the correct path, not
// from the root.
$base_uri = $this->getSubversionBaseURI();
$base_uri = new PhutilURI($base_uri);
$path = $base_uri->getPath();
if (!$path) {
$path = '/';
}
// If the trailing "@" is not required to escape the URI, strip it for
// readability.
if (!preg_match('/@.*@/', $path)) {
$path = rtrim($path, '@');
}
$base_uri->setPath($path);
return $base_uri;
} else {
return $this->getRemoteURIObject();
}
}
// TODO: This should be cleaned up to deal with all the new URI handling.
$another_copy = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needURIs(true)
->executeOne();
$clone_uris = $another_copy->getCloneURIs();
if (!$clone_uris) {
return null;
}
return head($clone_uris)->getEffectiveURI();
}
private function getRawHTTPCloneURIObject() {
$uri = PhabricatorEnv::getProductionURI($this->getURI());
$uri = new PhutilURI($uri);
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
return $uri;
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private function shouldUseSSH() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
if ($this->isSSHProtocol($protocol)) {
return true;
}
return false;
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private function shouldUseHTTP() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'http' || $protocol == 'https');
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private function shouldUseSVNProtocol() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'svn');
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private function isSSHProtocol($protocol) {
return ($protocol == 'ssh' || $protocol == 'svn+ssh');
}
public function delete() {
$this->openTransaction();
$paths = id(new PhabricatorOwnersPath())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($paths as $path) {
$path->delete();
}
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorRepositorySymbol())->getTableName(),
$this->getPHID());
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($commits as $commit) {
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit->delete();
}
$uris = id(new PhabricatorRepositoryURI())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($uris as $uri) {
$uri->delete();
}
$ref_cursors = id(new PhabricatorRepositoryRefCursor())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($ref_cursors as $cursor) {
$cursor->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_FILESYSTEM,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_PATHCHANGE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_SUMMARY,
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function isGit() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
public function isSVN() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
}
public function isHg() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
}
public function isHosted() {
return (bool)$this->getDetail('hosting-enabled', false);
}
public function setHosted($enabled) {
return $this->setDetail('hosting-enabled', $enabled);
}
public function canServeProtocol($protocol, $write) {
if (!$this->isTracked()) {
return false;
}
$clone_uris = $this->getCloneURIs();
foreach ($clone_uris as $uri) {
if ($uri->getBuiltinProtocol() !== $protocol) {
continue;
}
$io_type = $uri->getEffectiveIoType();
if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
return true;
}
if (!$write) {
if ($io_type == PhabricatorRepositoryURI::IO_READ) {
return true;
}
}
}
return false;
}
public function hasLocalWorkingCopy() {
try {
self::assertLocalExists();
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Raise more useful errors when there are basic filesystem problems.
*/
private function assertLocalExists() {
if (!$this->usesLocalWorkingCopy()) {
return;
}
$local = $this->getLocalPath();
Filesystem::assertExists($local);
Filesystem::assertIsDirectory($local);
Filesystem::assertReadable($local);
}
/**
* Determine if the working copy is bare or not. In Git, this corresponds
* to `--bare`. In Mercurial, `--noupdate`.
*/
public function isWorkingCopyBare() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return false;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$local = $this->getLocalPath();
if (Filesystem::pathExists($local.'/.git')) {
return false;
} else {
return true;
}
}
}
public function usesLocalWorkingCopy() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->isHosted();
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return true;
}
}
public function getHookDirectories() {
$directories = array();
if (!$this->isHosted()) {
return $directories;
}
$root = $this->getLocalPath();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($this->isWorkingCopyBare()) {
$directories[] = $root.'/hooks/pre-receive-phabricator.d/';
} else {
$directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$directories[] = $root.'/hooks/pre-commit-phabricator.d/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: We don't support custom Mercurial hooks for now because they're
// messy and we can't easily just drop a `hooks.d/` directory next to
// the hooks.
break;
}
return $directories;
}
public function canDestroyWorkingCopy() {
if ($this->isHosted()) {
// Never destroy hosted working copies.
return false;
}
$default_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
}
public function canUsePathTree() {
return !$this->isSVN();
}
public function canUseGitLFS() {
if (!$this->isGit()) {
return false;
}
if (!$this->isHosted()) {
return false;
}
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) {
return false;
}
return true;
}
public function getGitLFSURI($path = null) {
if (!$this->canUseGitLFS()) {
throw new Exception(
pht(
'This repository does not support Git LFS, so Git LFS URIs can '.
'not be generated for it.'));
}
$uri = $this->getRawHTTPCloneURIObject();
$uri = (string)$uri;
$uri = $uri.'/'.$path;
return $uri;
}
public function canMirror() {
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function canAllowDangerousChanges() {
if (!$this->isHosted()) {
return false;
}
// In Git and Mercurial, ref deletions and rewrites are dangerous.
// In Subversion, editing revprops is dangerous.
return true;
}
public function shouldAllowDangerousChanges() {
return (bool)$this->getDetail('allow-dangerous-changes');
}
public function canAllowEnormousChanges() {
if (!$this->isHosted()) {
return false;
}
return true;
}
public function shouldAllowEnormousChanges() {
return (bool)$this->getDetail('allow-enormous-changes');
}
public function writeStatusMessage(
$status_type,
$status_code,
array $parameters = array()) {
$table = new PhabricatorRepositoryStatusMessage();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
if ($status_code === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
$table_name,
$this->getID(),
$status_type);
} else {
// If the existing message has the same code (e.g., we just hit an
// error and also previously hit an error) we increment the message
// count. This allows us to determine how many times in a row we've
// run into an error.
// NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated
// in order, so the "messageCount" assignment must occur before the
// "statusCode" assignment. See T11705.
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, statusType, statusCode, parameters, epoch,
messageCount)
VALUES (%d, %s, %s, %s, %d, %d)
ON DUPLICATE KEY UPDATE
messageCount =
IF(
statusCode = VALUES(statusCode),
messageCount + VALUES(messageCount),
VALUES(messageCount)),
statusCode = VALUES(statusCode),
parameters = VALUES(parameters),
epoch = VALUES(epoch)',
$table_name,
$this->getID(),
$status_type,
$status_code,
json_encode($parameters),
time(),
1);
}
return $this;
}
public static function assertValidRemoteURI($uri) {
if (trim($uri) != $uri) {
throw new Exception(
pht('The remote URI has leading or trailing whitespace.'));
}
$uri_object = new PhutilURI($uri);
$protocol = $uri_object->getProtocol();
// Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
// for discussion. This is usually a user adding "ssh://" to an implicit
// SSH Git URI.
if ($protocol == 'ssh') {
if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
throw new Exception(
pht(
"The remote URI is not formatted correctly. Remote URIs ".
"with an explicit protocol should be in the form ".
"'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
'proto://domain/path',
'proto://domain:/path',
':/path'));
}
}
switch ($protocol) {
case 'ssh':
case 'http':
case 'https':
case 'git':
case 'svn':
case 'svn+ssh':
break;
default:
// NOTE: We're explicitly rejecting 'file://' because it can be
// used to clone from the working copy of another repository on disk
// that you don't normally have permission to access.
throw new Exception(
pht(
'The URI protocol is unrecognized. It should begin with '.
'"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
'ssh://',
'http://',
'https://',
'git://',
'svn://',
'svn+ssh://',
'git@domain.com:path'));
}
return true;
}
/**
* Load the pull frequency for this repository, based on the time since the
* last activity.
*
* We pull rarely used repositories less frequently. This finds the most
* recent commit which is older than the current time (which prevents us from
* spinning on repositories with a silly commit post-dated to some time in
* 2037). We adjust the pull frequency based on when the most recent commit
* occurred.
*
* @param int The minimum update interval to use, in seconds.
* @return int Repository update interval, in seconds.
*/
public function loadUpdateInterval($minimum = 1) { // c4science custo
// First, check if we've hit errors recently. If we have, wait one period
// for each consecutive error. Normally, this corresponds to a backoff of
// 15s, 30s, 45s, etc.
$message_table = new PhabricatorRepositoryStatusMessage();
$conn = $message_table->establishConnection('r');
$error_count = queryfx_one(
$conn,
'SELECT MAX(messageCount) error_count FROM %T
WHERE repositoryID = %d
AND statusType IN (%Ls)
AND statusCode IN (%Ls)',
$message_table->getTableName(),
$this->getID(),
array(
PhabricatorRepositoryStatusMessage::TYPE_INIT,
PhabricatorRepositoryStatusMessage::TYPE_FETCH,
),
array(
PhabricatorRepositoryStatusMessage::CODE_ERROR,
));
$error_count = (int)$error_count['error_count'];
if ($error_count > 0) {
return (int)($minimum * $error_count);
}
// If a repository is still importing, always pull it as frequently as
// possible. This prevents us from hanging for a long time at 99.9% when
// importing an inactive repository.
if ($this->isImporting()) {
return $minimum;
}
$window_start = (PhabricatorTime::getNow() + $minimum);
$table = id(new PhabricatorRepositoryCommit());
$last_commit = queryfx_one(
$table->establishConnection('r'),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1',
$table->getTableName(),
$this->getID(),
$window_start);
if ($last_commit) {
$time_since_commit = ($window_start - $last_commit['epoch']);
} else {
// If the repository has no commits, treat the creation date as
// though it were the date of the last commit. This makes empty
// repositories update quickly at first but slow down over time
// if they don't see any activity.
$time_since_commit = ($window_start - $this->getDateCreated());
}
$last_few_days = phutil_units('3 days in seconds');
if ($time_since_commit <= $last_few_days) {
// For repositories with activity in the recent past, we wait one
// extra second for every 10 minutes since the last commit. This
// shorter backoff is intended to handle weekends and other short
// breaks from development.
$smart_wait = ($time_since_commit / 600);
} else {
// For repositories without recent activity, we wait one extra second
// for every 4 minutes since the last commit. This longer backoff
// handles rarely used repositories, up to the maximum.
$smart_wait = ($time_since_commit / 240);
}
// We'll never wait more than 6 hours to pull a repository.
$longest_wait = phutil_units('6 hours in seconds');
$smart_wait = min($smart_wait, $longest_wait);
$smart_wait = max($minimum, $smart_wait);
return (int)$smart_wait;
}
/**
* Retrieve the service URI for the device hosting this repository.
*
* See @{method:newConduitClient} for a general discussion of interacting
* with repository services. This method provides lower-level resolution of
* services, returning raw URIs.
*
* @param PhabricatorUser Viewing user.
* @param map<string, wild> Constraints on selectable services.
* @return string|null URI, or `null` for local repositories.
*/
public function getAlmanacServiceURI(
PhabricatorUser $viewer,
array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'neverProxy' => 'bool',
'protocols' => 'list<string>',
'writable' => 'optional bool',
));
$never_proxy = $options['neverProxy'];
$protocols = $options['protocols'];
$writable = idx($options, 'writable', false);
$cache_key = $this->getAlmanacServiceCacheKey();
if (!$cache_key) {
return null;
}
$cache = PhabricatorCaches::getMutableStructureCache();
$uris = $cache->getKey($cache_key, false);
// If we haven't built the cache yet, build it now.
if ($uris === false) {
$uris = $this->buildAlmanacServiceURIs();
$cache->setKey($cache_key, $uris);
}
if ($uris === null) {
return null;
}
$local_device = AlmanacKeys::getDeviceID();
if ($never_proxy && !$local_device) {
throw new Exception(
pht(
'Unable to handle proxied service request. This device is not '.
'registered, so it can not identify local services. Register '.
'this device before sending requests here.'));
}
$protocol_map = array_fuse($protocols);
$results = array();
foreach ($uris as $uri) {
// If we're never proxying this and it's locally satisfiable, return
// `null` to tell the caller to handle it locally. If we're allowed to
// proxy, we skip this check and may proxy the request to ourselves.
// (That proxied request will end up here with proxying forbidden,
// return `null`, and then the request will actually run.)
if ($local_device && $never_proxy) {
if ($uri['device'] == $local_device) {
return null;
}
}
if (isset($protocol_map[$uri['protocol']])) {
$results[] = $uri;
}
}
if (!$results) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces which support the required protocols (%s).',
implode(', ', $protocols)));
}
if ($never_proxy) {
throw new Exception(
pht(
'Refusing to proxy a repository request from a cluster host. '.
'Cluster hosts must correctly route their intracluster requests.'));
}
if (count($results) > 1) {
if (!$this->supportsSynchronization()) {
throw new Exception(
pht(
'Repository "%s" is bound to multiple active repository hosts, '.
'but this repository does not support cluster synchronization. '.
'Declusterize this repository or move it to a service with only '.
'one host.',
$this->getDisplayName()));
}
}
// If we require a writable device, remove URIs which aren't writable.
if ($writable) {
foreach ($results as $key => $uri) {
if (!$uri['writable']) {
unset($results[$key]);
}
}
if (!$results) {
throw new Exception(
pht(
'This repository ("%s") is not writable with the given '.
'protocols (%s). The Almanac service for this repository has no '.
'writable bindings that support these protocols.',
$this->getDisplayName(),
implode(', ', $protocols)));
}
}
shuffle($results);
$result = head($results);
return $result['uri'];
}
public function supportsSynchronization() {
// TODO: For now, this is only supported for Git.
if (!$this->isGit()) {
return false;
}
return true;
}
public function getAlmanacServiceCacheKey() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
return null;
}
$repository_phid = $this->getPHID();
$parts = array(
"repo({$repository_phid})",
"serv({$service_phid})",
'v2',
);
return implode('.', $parts);
}
private function buildAlmanacServiceURIs() {
$service = $this->loadAlmanacService();
if (!$service) {
return null;
}
$bindings = $service->getActiveBindings();
if (!$bindings) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces.'));
}
$uris = array();
foreach ($bindings as $binding) {
$iface = $binding->getInterface();
$uri = $this->getClusterRepositoryURIFromBinding($binding);
$protocol = $uri->getProtocol();
$device_name = $iface->getDevice()->getName();
$uris[] = array(
'protocol' => $protocol,
'uri' => (string)$uri,
'device' => $device_name,
'writable' => (bool)$binding->getAlmanacPropertyValue('writable'),
);
}
return $uris;
}
/**
* Build a new Conduit client in order to make a service call to this
* repository.
*
* If the repository is hosted locally, this method may return `null`. The
* caller should use `ConduitCall` or other local logic to complete the
* request.
*
* By default, we will return a @{class:ConduitClient} for any repository with
* a service, even if that service is on the current device.
*
* We do this because this configuration does not make very much sense in a
* production context, but is very common in a test/development context
* (where the developer's machine is both the web host and the repository
* service). By proxying in development, we get more consistent behavior
* between development and production, and don't have a major untested
* codepath.
*
* The `$never_proxy` parameter can be used to prevent this local proxying.
* If the flag is passed:
*
* - The method will return `null` (implying a local service call)
* if the repository service is hosted on the current device.
* - The method will throw if it would need to return a client.
*
* This is used to prevent loops in Conduit: the first request will proxy,
* even in development, but the second request will be identified as a
* cluster request and forced not to proxy.
*
* For lower-level service resolution, see @{method:getAlmanacServiceURI}.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a client would be returned.
* @return ConduitClient|null Client, or `null` for local repositories.
*/
public function newConduitClient(
PhabricatorUser $viewer,
$never_proxy = false) {
$uri = $this->getAlmanacServiceURI(
$viewer,
array(
'neverProxy' => $never_proxy,
'protocols' => array(
'http',
'https',
),
// At least today, no Conduit call can ever write to a repository,
// so it's fine to send anything to a read-only node.
'writable' => false,
));
if ($uri === null) {
return null;
}
$domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
$client = id(new ConduitClient($uri))
->setHost($domain);
if ($viewer->isOmnipotent()) {
// If the caller is the omnipotent user (normally, a daemon), we will
// sign the request with this host's asymmetric keypair.
$public_path = AlmanacKeys::getKeyPath('device.pub');
try {
$public_key = Filesystem::readFile($public_path);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device public key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
'Use `%s` to register keys for this device. Exception: %s',
'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$private_path = AlmanacKeys::getKeyPath('device.key');
try {
$private_key = Filesystem::readFile($private_path);
$private_key = new PhutilOpaqueEnvelope($private_key);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device private key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
'Use `%s` to register keys for this device. Exception: %s',
'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$client->setSigningKeys($public_key, $private_key);
} else {
// If the caller is a normal user, we generate or retrieve a cluster
// API token.
$token = PhabricatorConduitToken::loadClusterTokenForUser($viewer);
if ($token) {
$client->setConduitToken($token->getToken());
}
}
return $client;
}
public function getPassthroughEnvironmentalVariables() {
$env = $_ENV;
if ($this->isGit()) {
// $_ENV does not populate in CLI contexts if "E" is missing from
// "variables_order" in PHP config. Currently, we do not require this
// to be configured. Since it may not be, explicitly bring expected Git
// environmental variables into scope. This list is not exhaustive, but
// only lists variables with a known impact on commit hook behavior.
// This can be removed if we later require "E" in "variables_order".
$git_env = array(
'GIT_OBJECT_DIRECTORY',
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_QUARANTINE_PATH',
);
foreach ($git_env as $key) {
$value = getenv($key);
if (strlen($value)) {
$env[$key] = $value;
}
}
$key = 'GIT_PUSH_OPTION_COUNT';
$git_count = getenv($key);
if (strlen($git_count)) {
$git_count = (int)$git_count;
$env[$key] = $git_count;
for ($ii = 0; $ii < $git_count; $ii++) {
$key = 'GIT_PUSH_OPTION_'.$ii;
$env[$key] = getenv($key);
}
}
}
$result = array();
foreach ($env as $key => $value) {
// In Git, pass anything matching "GIT_*" though. Some of these variables
// need to be preserved to allow `git` operations to work properly when
// running from commit hooks.
if ($this->isGit()) {
if (preg_match('/^GIT_/', $key)) {
$result[$key] = $value;
}
}
}
return $result;
}
public function supportsBranchComparison() {
return $this->isGit();
}
/* -( Repository URIs )---------------------------------------------------- */
public function attachURIs(array $uris) {
$custom_map = array();
foreach ($uris as $key => $uri) {
$builtin_key = $uri->getRepositoryURIBuiltinKey();
if ($builtin_key !== null) {
$custom_map[$builtin_key] = $key;
}
}
$builtin_uris = $this->newBuiltinURIs();
$seen_builtins = array();
foreach ($builtin_uris as $builtin_uri) {
$builtin_key = $builtin_uri->getRepositoryURIBuiltinKey();
$seen_builtins[$builtin_key] = true;
// If this builtin URI is disabled, don't attach it and remove the
// persisted version if it exists.
if ($builtin_uri->getIsDisabled()) {
if (isset($custom_map[$builtin_key])) {
unset($uris[$custom_map[$builtin_key]]);
}
continue;
}
// If the URI exists, make sure it's marked as not being disabled.
if (isset($custom_map[$builtin_key])) {
$uris[$custom_map[$builtin_key]]->setIsDisabled(false);
}
}
// Remove any builtins which no longer exist.
foreach ($custom_map as $builtin_key => $key) {
if (empty($seen_builtins[$builtin_key])) {
unset($uris[$key]);
}
}
$this->uris = $uris;
return $this;
}
public function getURIs() {
return $this->assertAttached($this->uris);
}
public function getCloneURIs() {
$uris = $this->getURIs();
$clone = array();
foreach ($uris as $uri) {
if (!$uri->isBuiltin()) {
continue;
}
if ($uri->getIsDisabled()) {
continue;
}
$io_type = $uri->getEffectiveIoType();
$is_clone =
($io_type == PhabricatorRepositoryURI::IO_READ) ||
($io_type == PhabricatorRepositoryURI::IO_READWRITE);
if (!$is_clone) {
continue;
}
$clone[] = $uri;
}
$clone = msort($clone, 'getURIScore');
$clone = array_reverse($clone);
return $clone;
}
public function newBuiltinURIs() {
$has_callsign = ($this->getCallsign() !== null);
$has_shortname = ($this->getRepositorySlug() !== null);
$identifier_map = array(
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true,
);
// If the view policy of the repository is public, support anonymous HTTP
// even if authenticated HTTP is not supported.
if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) {
$allow_http = true;
} else {
$allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
}
$base_uri = PhabricatorEnv::getURI('/');
$base_uri = new PhutilURI($base_uri);
$has_https = ($base_uri->getProtocol() == 'https');
$has_https = ($has_https && $allow_http);
$has_http = !PhabricatorEnv::getEnvConfig('security.require-https');
$has_http = ($has_http && $allow_http);
// HTTP is not supported for Subversion.
if ($this->isSVN()) {
$has_http = false;
$has_https = false;
}
$has_ssh = (bool)strlen(PhabricatorEnv::getEnvConfig('phd.user'));
$protocol_map = array(
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http,
);
$uris = array();
foreach ($protocol_map as $protocol => $proto_supported) {
foreach ($identifier_map as $identifier => $id_supported) {
// This is just a dummy value because it can't be empty; we'll force
// it to a proper value when using it in the UI.
$builtin_uri = "{$protocol}://{$identifier}";
$uris[] = PhabricatorRepositoryURI::initializeNewURI()
->setRepositoryPHID($this->getPHID())
->attachRepository($this)
->setBuiltinProtocol($protocol)
->setBuiltinIdentifier($identifier)
->setURI($builtin_uri)
->setIsDisabled((int)(!$proto_supported || !$id_supported));
}
}
return $uris;
}
public function getClusterRepositoryURIFromBinding(
AlmanacBinding $binding) {
$protocol = $binding->getAlmanacPropertyValue('protocol');
if ($protocol === null) {
$protocol = 'https';
}
$iface = $binding->getInterface();
$address = $iface->renderDisplayAddress();
$path = $this->getURI();
return id(new PhutilURI("{$protocol}://{$address}"))
->setPath($path);
}
public function loadAlmanacService() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
// No service, so this is a local repository.
return null;
}
$service = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($service_phid))
->needBindings(true)
->needProperties(true)
->executeOne();
if (!$service) {
throw new Exception(
pht(
'The Almanac service for this repository is invalid or could not '.
'be loaded.'));
}
$service_type = $service->getServiceImplementation();
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
throw new Exception(
pht(
'The Almanac service for this repository does not have the correct '.
'service type.'));
}
return $service;
}
public function markImporting() {
$this->openTransaction();
$this->beginReadLocking();
$repository = $this->reload();
$repository->setDetail('importing', true);
$repository->save();
$this->endReadLocking();
$this->saveTransaction();
return $repository;
}
/* -( Symbols )-------------------------------------------------------------*/
public function getSymbolSources() {
return $this->getDetail('symbol-sources', array());
}
public function getSymbolLanguages() {
return $this->getDetail('symbol-languages', array());
}
/* -( Staging )------------------------------------------------------------ */
public function supportsStaging() {
return $this->isGit();
}
public function getStagingURI() {
if (!$this->supportsStaging()) {
return null;
}
return $this->getDetail('staging-uri', null);
}
/* -( Automation )--------------------------------------------------------- */
public function supportsAutomation() {
return $this->isGit();
}
public function canPerformAutomation() {
if (!$this->supportsAutomation()) {
return false;
}
if (!$this->getAutomationBlueprintPHIDs()) {
return false;
}
return true;
}
public function getAutomationBlueprintPHIDs() {
if (!$this->supportsAutomation()) {
return array();
}
return $this->getDetail('automation.blueprintPHIDs', array());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorRepositoryEditor();
}
public function 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;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
return "repo:{$hash}";
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
return $this->getDetail('description');
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return true;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$phid = $this->getPHID();
$this->openTransaction();
$this->delete();
PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array());
$books = id(new DivinerBookQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($books as $book) {
$engine->destroyObject($book);
}
$atoms = id(new DivinerAtomQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($atoms as $atom) {
$engine->destroyObject($atom);
}
$lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($lfs_refs as $ref) {
$engine->destroyObject($ref);
}
$this->saveTransaction();
}
/* -( PhabricatorDestructibleCodexInterface )------------------------------ */
public function newDestructibleCodex() {
return new PhabricatorRepositoryDestructibleCodex();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The repository name.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('vcs')
->setType('string')
->setDescription(
pht('The VCS this repository uses ("git", "hg" or "svn").')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('callsign')
->setType('string')
->setDescription(pht('The repository callsign, if it has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('shortName')
->setType('string')
->setDescription(pht('Unique short name, if the repository has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or inactive status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isImporting')
->setType('bool')
->setDescription(
pht(
'True if the repository is importing initial commits.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'vcs' => $this->getVersionControlSystem(),
'callsign' => $this->getCallsign(),
'shortName' => $this->getRepositorySlug(),
'status' => $this->getStatus(),
'isImporting' => (bool)$this->isImporting(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DiffusionRepositoryURIsSearchEngineAttachment())
->setAttachmentKey('uris'),
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorRepositoryFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorRepositoryFerretEngine();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
index 01088522e..447db08e6 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -1,744 +1,866 @@
<?php
final class PhabricatorRepositoryCommit
extends PhabricatorRepositoryDAO
implements
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorProjectInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
HarbormasterBuildkiteBuildableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorDraftInterface {
protected $repositoryID;
protected $phid;
+ protected $authorIdentityPHID;
+ protected $committerIdentityPHID;
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;
const IMPORTED_UNREACHABLE = 2048;
private $commitData = self::ATTACHABLE;
private $audits = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
+ private $authorIdentity = self::ATTACHABLE;
+ private $committerIdentity = self::ATTACHABLE;
+
private $drafts = array();
private $auditAuthorityPHIDs = array();
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 isUnreachable() {
return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE);
}
public function writeImportStatusFlag($flag) {
return $this->adjustImportStatusFlag($flag, true);
}
public function clearImportStatusFlag($flag) {
return $this->adjustImportStatusFlag($flag, false);
}
private function adjustImportStatusFlag($flag, $set) {
$conn_w = $this->establishConnection('w');
$table_name = $this->getTableName();
$id = $this->getID();
if ($set) {
queryfx(
$conn_w,
'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d',
$table_name,
$flag,
$id);
$this->setImportStatus($this->getImportStatus() | $flag);
} else {
queryfx(
$conn_w,
'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d',
$table_name,
$flag,
$id);
$this->setImportStatus($this->getImportStatus() & ~$flag);
}
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'commitIdentifier' => 'text40',
'mailKey' => 'bytes20',
'authorPHID' => 'phid?',
+ 'authorIdentityPHID' => 'phid?',
+ 'committerIdentityPHID' => 'phid?',
'auditStatus' => 'uint32',
'summary' => 'text255',
'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,
),
'key_epoch' => array(
'columns' => array('epoch'),
),
'key_author' => array(
'columns' => array('authorPHID', 'epoch'),
),
),
self::CONFIG_NO_MUTATE => array(
'importStatus',
),
) + 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 hasAttachedAudits() {
return ($this->audits !== self::ATTACHABLE);
}
+ public function attachIdentities(
+ PhabricatorRepositoryIdentity $author = null,
+ PhabricatorRepositoryIdentity $committer = null) {
+
+ $this->authorIdentity = $author;
+ $this->committerIdentity = $committer;
+
+ return $this;
+ }
+
+ public function getAuthorIdentity() {
+ return $this->assertAttached($this->authorIdentity);
+ }
+
+ public function getCommitterIdentity() {
+ return $this->assertAttached($this->committerIdentity);
+ }
+
public function loadAndAttachAuditAuthority(
PhabricatorUser $viewer,
$actor_phid = null) {
if ($actor_phid === null) {
$actor_phid = $viewer->getPHID();
}
// TODO: This method is a little weird and sketchy, but worlds better than
// what came before it. Eventually, this should probably live in a Query
// class.
// Figure out which requests the actor has authority over: these are user
// requests where they are the auditor, and packages and projects they are
// a member of.
if (!$actor_phid) {
$attach_key = $viewer->getCacheFragment();
$phids = array();
} else {
$attach_key = $actor_phid;
// At least currently, when modifying your own commits, you act only on
// behalf of yourself, not your packages/projects -- the idea being that
// you can't accept your own commits. This may change or depend on
// config.
$actor_is_author = ($actor_phid == $this->getAuthorPHID());
if ($actor_is_author) {
$phids = array($actor_phid);
} else {
$phids = array();
$phids[$actor_phid] = true;
$owned_packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withAuthorityPHIDs(array($actor_phid))
->execute();
foreach ($owned_packages as $package) {
$phids[$package->getPHID()] = true;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($actor_phid))
->execute();
foreach ($projects as $project) {
$phids[$project->getPHID()] = true;
}
$phids = array_keys($phids);
}
}
$this->auditAuthorityPHIDs[$attach_key] = array_fuse($phids);
return $this;
}
public function hasAuditAuthority(
PhabricatorUser $viewer,
PhabricatorRepositoryAuditRequest $audit,
$actor_phid = null) {
if ($actor_phid === null) {
$actor_phid = $viewer->getPHID();
}
if (!$actor_phid) {
$attach_key = $viewer->getCacheFragment();
} else {
$attach_key = $actor_phid;
}
$map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $attach_key);
if (!$actor_phid) {
return false;
}
return isset($map[$audit->getAuditorPHID()]);
}
public function writeOwnersEdges(array $package_phids) {
$src_phid = $this->getPHID();
$edge_type = DiffusionCommitHasPackageEdgeType::EDGECONST;
$editor = new PhabricatorEdgeEditor();
$dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$src_phid,
$edge_type);
foreach ($dst_phids as $dst_phid) {
$editor->removeEdge($src_phid, $edge_type, $dst_phid);
}
foreach ($package_phids as $package_phid) {
$editor->addEdge($src_phid, $edge_type, $package_phid);
}
$editor->save();
return $this;
}
public function getAuditorPHIDsForEdit() {
$audits = $this->getAudits();
return mpull($audits, 'getAuditorPHID');
}
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() {
return '/'.$this->getMonogram();
}
/**
* 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:
case PhabricatorAuditStatusConstants::AUDIT_REQUESTED:
$any_need = true;
break;
case PhabricatorAuditStatusConstants::ACCEPTED:
$any_accept = true;
break;
case PhabricatorAuditStatusConstants::CONCERNED:
$any_concern = true;
break;
}
}
$current_status = $this->getAuditStatus();
$status_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION;
if ($any_concern) {
if ($current_status == $status_verify) {
// If the change is in "Needs Verification", we keep it there as
// long as any auditors still have concerns.
$status = $status_verify;
} else {
$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);
}
public function getMonogram() {
$repository = $this->getRepository();
$callsign = $repository->getCallsign();
$identifier = $this->getCommitIdentifier();
if ($callsign !== null) {
return "r{$callsign}{$identifier}";
} else {
$id = $repository->getID();
return "R{$id}:{$identifier}";
}
}
public function getDisplayName() {
$repository = $this->getRepository();
$identifier = $this->getCommitIdentifier();
return $repository->formatCommitName($identifier);
}
/**
* Return a local display name for use in the context of the containing
* repository.
*
* In Git and Mercurial, this returns only a short hash, like "abcdef012345".
* See @{method:getDisplayName} for a short name that always includes
* repository context.
*
* @return string Short human-readable name for use inside a repository.
*/
public function getLocalName() {
$repository = $this->getRepository();
$identifier = $this->getCommitIdentifier();
return $repository->formatCommitName($identifier, $local = true);
}
+ /**
+ * Make a strong effort to find a way to render this commit's committer.
+ * This currently attempts to use @{PhabricatorRepositoryIdentity}, and
+ * falls back to examining the commit detail information. After we force
+ * the migration to using identities, update this method to remove the
+ * fallback. See T12164 for details.
+ */
+ public function renderAnyCommitter(PhabricatorUser $viewer, $handles) {
+ $committer = $this->renderCommitter($viewer, $handles);
+ if ($committer) {
+ return $committer;
+ }
+
+ return $this->renderAuthor($viewer, $handles);
+ }
+
+ public function renderCommitter(PhabricatorUser $viewer, $handles) {
+ $committer_phid = $this->getCommitterDisplayPHID();
+ if ($committer_phid) {
+ return $handles[$committer_phid]->renderLink();
+ }
+
+ $data = $this->getCommitData();
+ $committer_name = $data->getCommitDetail('committer');
+ if (strlen($committer_name)) {
+ return DiffusionView::renderName($committer_name);
+ }
+
+ return null;
+ }
+
+ public function renderAuthor(PhabricatorUser $viewer, $handles) {
+ $author_phid = $this->getAuthorDisplayPHID();
+ if ($author_phid) {
+ return $handles[$author_phid]->renderLink();
+ }
+
+ $data = $this->getCommitData();
+ $author_name = $data->getAuthorName();
+ if (strlen($author_name)) {
+ return DiffusionView::renderName($author_name);
+ }
+
+ return null;
+ }
+
+ public function loadIdentities(PhabricatorUser $viewer) {
+ if ($this->authorIdentity !== self::ATTACHABLE) {
+ return $this;
+ }
+
+ $commit = id(new DiffusionCommitQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($this->getID()))
+ ->needIdentities(true)
+ ->executeOne();
+
+ $author_identity = $commit->getAuthorIdentity();
+ $committer_identity = $commit->getCommitterIdentity();
+
+ return $this->attachIdentities($author_identity, $committer_identity);
+ }
+
+ public function hasCommitterIdentity() {
+ return ($this->getCommitterIdentity() !== null);
+ }
+
+ public function hasAuthorIdentity() {
+ return ($this->getAuthorIdentity() !== null);
+ }
+
+ public function getCommitterDisplayPHID() {
+ if ($this->hasCommitterIdentity()) {
+ return $this->getCommitterIdentity()->getIdentityDisplayPHID();
+ }
+
+ $data = $this->getCommitData();
+ return $data->getCommitDetail('committerPHID');
+ }
+
+ public function getAuthorDisplayPHID() {
+ if ($this->hasAuthorIdentity()) {
+ return $this->getAuthorIdentity()->getIdentityDisplayPHID();
+ }
+
+ $data = $this->getCommitData();
+ return $data->getCommitDetail('authorPHID');
+ }
+
+ public function getAuditStatusObject() {
+ $status = $this->getAuditStatus();
+ return PhabricatorAuditCommitStatusConstants::newForLegacyStatus($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:
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 getHarbormasterBuildableDisplayPHID() {
return $this->getHarbormasterBuildablePHID();
}
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.phid'] = $repo->getPHID();
$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.phid' =>
pht('The PHID of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
);
}
public function newBuildableEngine() {
return new DiffusionBuildableEngine();
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$repository = $this->getRepository();
$commit_phid = $this->getPHID();
$repository_phid = $repository->getPHID();
if ($repository->isHosted()) {
throw new Exception(
pht(
'This commit ("%s") is associated with a hosted repository '.
'("%s"). Repositories must be imported from GitHub to be built '.
'with CircleCI.',
$commit_phid,
$repository_phid));
}
$remote_uri = $repository->getRemoteURI();
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$remote_uri);
if (!$path) {
throw new Exception(
pht(
'This commit ("%s") is associated with a repository ("%s") that '.
'with a remote URI ("%s") that does not appear to be hosted on '.
'GitHub. Repositories must be hosted on GitHub to be built with '.
'CircleCI.',
$commit_phid,
$repository_phid,
$remote_uri));
}
return $remote_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'revision';
}
public function getCircleCIBuildIdentifier() {
return $this->getCommitIdentifier();
}
/* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */
public function getBuildkiteBranch() {
$viewer = PhabricatorUser::getOmnipotentUser();
$repository = $this->getRepository();
$branches = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'user' => $viewer,
)),
'diffusion.branchquery',
array(
'contains' => $this->getCommitIdentifier(),
'repository' => $repository->getPHID(),
));
if (!$branches) {
throw new Exception(
pht(
'Commit "%s" is not an ancestor of any branch head, so it can not '.
'be built with Buildkite.',
$this->getCommitIdentifier()));
}
$branch = head($branches);
return 'refs/heads/'.$branch['shortName'];
}
public function getBuildkiteCommit() {
return $this->getCommitIdentifier();
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('diffusion.fields');
}
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. (It should not
// include resigned auditors.)
return ($phid == $this->getAuthorPHID());
}
/* -( 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) {
$xactions = $timeline->getTransactions();
$path_ids = array();
foreach ($xactions as $xaction) {
if ($xaction->hasComment()) {
$path_id = $xaction->getComment()->getPathID();
if ($path_id) {
$path_ids[] = $path_id;
}
}
}
$path_map = array();
if ($path_ids) {
$path_map = id(new DiffusionPathQuery())
->withPathIDs($path_ids)
->execute();
$path_map = ipull($path_map, 'path', 'id');
}
return $timeline->setPathMap($path_map);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DiffusionCommitFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new DiffusionCommitFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('identifier')
->setType('string')
->setDescription(pht('The commit identifier.')),
);
}
public function getFieldValuesForConduit() {
+
+ // NOTE: This data should be similar to the information returned about
+ // commmits by "differential.diff.search" with the "commits" attachment.
+
return array(
'identifier' => $this->getCommitIdentifier(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DiffusionCommitDraftEngine();
}
public function getHasDraft(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
}
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
return $this;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
new file mode 100644
index 000000000..416d1a3cd
--- /dev/null
+++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
@@ -0,0 +1,152 @@
+<?php
+
+final class PhabricatorRepositoryIdentity
+ extends PhabricatorRepositoryDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorApplicationTransactionInterface {
+
+ protected $authorPHID;
+ protected $identityNameHash;
+ protected $identityNameRaw;
+ protected $identityNameEncoding;
+ protected $automaticGuessedUserPHID;
+ protected $manuallySetUserPHID;
+ protected $currentEffectiveUserPHID;
+
+ private $effectiveUser = self::ATTACHABLE;
+
+ public function attachEffectiveUser(PhabricatorUser $user) {
+ $this->effectiveUser = $user;
+ return $this;
+ }
+
+ public function getEffectiveUser() {
+ return $this->assertAttached($this->effectiveUser);
+ }
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_BINARY => array(
+ 'identityNameRaw' => true,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'identityNameHash' => 'bytes12',
+ 'identityNameEncoding' => 'text16?',
+ 'automaticGuessedUserPHID' => 'phid?',
+ 'manuallySetUserPHID' => 'phid?',
+ 'currentEffectiveUserPHID' => 'phid?',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_identity' => array(
+ 'columns' => array('identityNameHash'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return PhabricatorRepositoryIdentityPHIDType::TYPECONST;
+ }
+
+ public function setIdentityName($name_raw) {
+ $this->setIdentityNameRaw($name_raw);
+ $this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw));
+ $this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw));
+
+ return $this;
+ }
+
+ public function getIdentityName() {
+ return $this->getUTF8StringFromStorage(
+ $this->getIdentityNameRaw(),
+ $this->getIdentityNameEncoding());
+ }
+
+ public function getIdentityEmailAddress() {
+ $address = new PhutilEmailAddress($this->getIdentityName());
+ return $address->getAddress();
+ }
+
+ public function getIdentityDisplayName() {
+ $address = new PhutilEmailAddress($this->getIdentityName());
+ return $address->getDisplayName();
+ }
+
+ public function getIdentityShortName() {
+ // TODO
+ return $this->getIdentityName();
+ }
+
+ public function getURI() {
+ return '/diffusion/identity/view/'.$this->getID().'/';
+ }
+
+ public function hasEffectiveUser() {
+ return ($this->currentEffectiveUserPHID != null);
+ }
+
+ public function getIdentityDisplayPHID() {
+ if ($this->hasEffectiveUser()) {
+ return $this->getCurrentEffectiveUserPHID();
+ } else {
+ return $this->getPHID();
+ }
+ }
+
+ public function save() {
+ if ($this->manuallySetUserPHID) {
+ $this->currentEffectiveUserPHID = $this->manuallySetUserPHID;
+ } else {
+ $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID;
+ }
+
+ return parent::save();
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ return PhabricatorPolicies::getMostOpenPolicy();
+ }
+
+ public function hasAutomaticCapability(
+ $capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new DiffusionRepositoryIdentityEditor();
+ }
+
+ public function getApplicationTransactionObject() {
+ return $this;
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorRepositoryIdentityTransaction();
+ }
+
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
+}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php
new file mode 100644
index 000000000..5f25346b6
--- /dev/null
+++ b/src/applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorRepositoryIdentityTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationTransactionType() {
+ return PhabricatorRepositoryIdentityPHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorRepositoryIdentityTransactionType';
+ }
+
+ public function getApplicationName() {
+ return 'repository';
+ }
+
+}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
index e44f99df4..682b36792 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
@@ -1,103 +1,106 @@
<?php
/**
* Groups a set of push logs corresponding to changes which were all pushed in
* the same transaction.
*/
final class PhabricatorRepositoryPushEvent
extends PhabricatorRepositoryDAO
implements PhabricatorPolicyInterface {
protected $repositoryPHID;
protected $epoch;
protected $pusherPHID;
protected $requestIdentifier;
protected $remoteAddress;
protected $remoteProtocol;
protected $rejectCode;
protected $rejectDetails;
protected $writeWait;
protected $readWait;
protected $hostWait;
private $repository = self::ATTACHABLE;
private $logs = self::ATTACHABLE;
public static function initializeNewEvent(PhabricatorUser $viewer) {
return id(new PhabricatorRepositoryPushEvent())
->setPusherPHID($viewer->getPHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'requestIdentifier' => 'bytes12?',
'remoteAddress' => 'ipaddress?',
'remoteProtocol' => 'text32?',
'rejectCode' => 'uint32',
'rejectDetails' => 'text64?',
'writeWait' => 'uint64?',
'readWait' => 'uint64?',
'hostWait' => 'uint64?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_repository' => array(
'columns' => array('repositoryPHID'),
),
'key_identifier' => array(
'columns' => array('requestIdentifier'),
),
+ 'key_reject' => array(
+ 'columns' => array('rejectCode', 'rejectDetails'),
+ ),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryPushEventPHIDType::TYPECONST);
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachLogs(array $logs) {
$this->logs = $logs;
return $this;
}
public function getLogs() {
return $this->assertAttached($this->logs);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getRepository()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getRepository()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
"A repository's push events are visible to users who can see the ".
"repository.");
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php
new file mode 100644
index 000000000..3c129845c
--- /dev/null
+++ b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorRepositoryIdentityChangeWorker
+extends PhabricatorWorker {
+
+ protected function doWork() {
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ $task_data = $this->getTaskData();
+ $user_phid = idx($task_data, 'userPHID');
+
+ $user = id(new PhabricatorPeopleQuery())
+ ->withPHIDs(array($user_phid))
+ ->setViewer($viewer)
+ ->executeOne();
+
+ $emails = id(new PhabricatorUserEmail())->loadAllWhere(
+ 'userPHID = %s ORDER BY address',
+ $user->getPHID());
+
+ foreach ($emails as $email) {
+ $identities = id(new PhabricatorRepositoryIdentityQuery())
+ ->setViewer($viewer)
+ ->withEmailAddress($email->getAddress())
+ ->execute();
+
+ foreach ($identities as $identity) {
+ $identity->setAutomaticGuessedUserPHID($user->getPHID())
+ ->save();
+ }
+ }
+ }
+
+}
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
index fe8abd592..8a1b5e5dd 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
@@ -1,412 +1,452 @@
<?php
abstract class PhabricatorRepositoryCommitMessageParserWorker
extends PhabricatorRepositoryCommitParserWorker {
protected function getImportStepFlag() {
return PhabricatorRepositoryCommit::IMPORTED_MESSAGE;
}
abstract protected function getFollowupTaskClass();
final protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
if (!$this->shouldSkipImportStep()) {
$viewer = PhabricatorUser::getOmnipotentUser();
$refs_raw = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'user' => $viewer,
)),
'diffusion.querycommits',
array(
'repositoryPHID' => $repository->getPHID(),
'phids' => array($commit->getPHID()),
'bypassCache' => true,
'needMessages' => true,
));
if (empty($refs_raw['data'])) {
throw new Exception(
pht(
'Unable to retrieve details for commit "%s"!',
$commit->getPHID()));
}
$ref = DiffusionCommitRef::newFromConduitResult(head($refs_raw['data']));
$this->updateCommitData($ref);
}
if ($this->shouldQueueFollowupTasks()) {
$this->queueTask(
$this->getFollowupTaskClass(),
array(
'commitID' => $commit->getID(),
),
array(
// We queue followup tasks at default priority so that the queue
// finishes work it has started before starting more work. If
// followups are queued at the same priority level, we do all
// message parses first, then all change parses, etc. This makes
// progress uneven. See T11677 for discussion.
'priority' => PhabricatorWorker::PRIORITY_DEFAULT,
));
}
}
final protected function updateCommitData(DiffusionCommitRef $ref) {
$commit = $this->commit;
$author = $ref->getAuthor();
$message = $ref->getMessage();
$committer = $ref->getCommitter();
$hashes = $ref->getHashes();
+ $author_identity = id(new PhabricatorRepositoryIdentityQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withIdentityNames(array($author))
+ ->executeOne();
+
+ if (!$author_identity) {
+ $author_identity = id(new PhabricatorRepositoryIdentity())
+ ->setAuthorPHID($commit->getPHID())
+ ->setIdentityName($author)
+ ->setAutomaticGuessedUserPHID(
+ $this->resolveUserPHID($commit, $author))
+ ->save();
+ }
+
+ $committer_identity = null;
+
+ if ($committer) {
+ $committer_identity = id(new PhabricatorRepositoryIdentityQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withIdentityNames(array($committer))
+ ->executeOne();
+
+ if (!$committer_identity) {
+ $committer_identity = id(new PhabricatorRepositoryIdentity())
+ ->setAuthorPHID($commit->getPHID())
+ ->setIdentityName($committer)
+ ->setAutomaticGuessedUserPHID(
+ $this->resolveUserPHID($commit, $committer))
+ ->save();
+ }
+ }
+
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
$data = new PhabricatorRepositoryCommitData();
}
$data->setCommitID($commit->getID());
$data->setAuthorName(id(new PhutilUTF8StringTruncator())
->setMaximumBytes(255)
->truncateString((string)$author));
$data->setCommitDetail('authorEpoch', $ref->getAuthorEpoch());
$data->setCommitDetail('authorName', $ref->getAuthorName());
$data->setCommitDetail('authorEmail', $ref->getAuthorEmail());
+ $data->setCommitDetail(
+ 'authorIdentityPHID', $author_identity->getPHID());
$data->setCommitDetail(
'authorPHID',
$this->resolveUserPHID($commit, $author));
$data->setCommitMessage($message);
if (strlen($committer)) {
$data->setCommitDetail('committer', $committer);
$data->setCommitDetail('committerName', $ref->getCommitterName());
$data->setCommitDetail('committerEmail', $ref->getCommitterEmail());
$data->setCommitDetail(
'committerPHID',
$this->resolveUserPHID($commit, $committer));
+ $data->setCommitDetail(
+ 'committerIdentityPHID', $committer_identity->getPHID());
+
+ $commit->setCommitterIdentityPHID($committer_identity->getPHID());
}
$repository = $this->repository;
$author_phid = $data->getCommitDetail('authorPHID');
$committer_phid = $data->getCommitDetail('committerPHID');
$user = new PhabricatorUser();
if ($author_phid) {
$user = $user->loadOneWhere(
'phid = %s',
$author_phid);
}
$differential_app = 'PhabricatorDifferentialApplication';
$revision_id = null;
$low_level_query = null;
if (PhabricatorApplication::isClassInstalled($differential_app)) {
$low_level_query = id(new DiffusionLowLevelCommitFieldsQuery())
->setRepository($repository)
->withCommitRef($ref);
$field_values = $low_level_query->execute();
$revision_id = idx($field_values, 'revisionID');
if (!empty($field_values['reviewedByPHIDs'])) {
$data->setCommitDetail(
'reviewerPHID',
reset($field_values['reviewedByPHIDs']));
}
$data->setCommitDetail('differential.revisionID', $revision_id);
}
if ($author_phid != $commit->getAuthorPHID()) {
$commit->setAuthorPHID($author_phid);
}
+ $commit->setAuthorIdentityPHID($author_identity->getPHID());
+
$commit->setSummary($data->getSummary());
$commit->save();
// Figure out if we're going to try to "autoclose" related objects (e.g.,
// close linked tasks and related revisions) and, if not, record why we
// aren't. Autoclose can be disabled for various reasons at the repository
// or commit levels.
$force_autoclose = idx($this->getTaskData(), 'forceAutoclose', false);
if ($force_autoclose) {
$autoclose_reason = PhabricatorRepository::BECAUSE_AUTOCLOSE_FORCED;
} else {
$autoclose_reason = $repository->shouldSkipAutocloseCommit($commit);
}
$data->setCommitDetail('autocloseReason', $autoclose_reason);
$should_autoclose = $force_autoclose ||
$repository->shouldAutocloseCommit($commit);
// When updating related objects, we'll act under an omnipotent user to
// ensure we can see them, but take actions as either the committer or
// author (if we recognize their accounts) or the Diffusion application
// (if we do not).
$actor = PhabricatorUser::getOmnipotentUser();
$acting_as_phid = nonempty(
$committer_phid,
$author_phid,
id(new PhabricatorDiffusionApplication())->getPHID());
$acting_user = $this->loadActingUser($actor, $acting_as_phid);
$conn_w = id(new DifferentialRevision())->establishConnection('w');
// NOTE: The `differential_commit` table has a unique ID on `commitPHID`,
// preventing more than one revision from being associated with a commit.
// Generally this is good and desirable, but with the advent of hash
// tracking we may end up in a situation where we match several different
// revisions. We just kind of ignore this and pick one, we might want to
// revisit this and do something differently. (If we match several revisions
// someone probably did something very silly, though.)
$revision = null;
if ($revision_id) {
$revision_query = id(new DifferentialRevisionQuery())
->withIDs(array($revision_id))
->setViewer($actor)
->needReviewers(true)
->needActiveDiffs(true);
$revision = $revision_query->executeOne();
if ($revision) {
$commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($commit->getPHID(), $commit_drev, $revision->getPHID())
->save();
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)',
DifferentialRevision::TABLE_COMMIT,
$revision->getID(),
$commit->getPHID());
$should_close = !$revision->isPublished() && $should_autoclose;
if ($should_close) {
$type_close = DifferentialRevisionCloseTransaction::TRANSACTIONTYPE;
$commit_close_xaction = id(new DifferentialTransaction())
->setTransactionType($type_close)
->setNewValue(true)
->setMetadataValue('isCommitClose', true);
$commit_close_xaction->setMetadataValue(
'commitPHID',
$commit->getPHID());
$commit_close_xaction->setMetadataValue(
'committerPHID',
$committer_phid);
$commit_close_xaction->setMetadataValue(
'committerName',
$data->getCommitDetail('committer'));
$commit_close_xaction->setMetadataValue(
'authorPHID',
$author_phid);
$commit_close_xaction->setMetadataValue(
'authorName',
$data->getAuthorName());
if ($low_level_query) {
$commit_close_xaction->setMetadataValue(
'revisionMatchData',
$low_level_query->getRevisionMatchData());
$data->setCommitDetail(
'revisionMatchData',
$low_level_query->getRevisionMatchData());
}
$extraction_engine = id(new DifferentialDiffExtractionEngine())
->setViewer($actor)
->setAuthorPHID($acting_as_phid);
$content_source = $this->newContentSource();
$update_data = $extraction_engine->updateRevisionWithCommit(
$revision,
$commit,
array(
$commit_close_xaction,
),
$content_source);
foreach ($update_data as $key => $value) {
$data->setCommitDetail($key, $value);
}
}
}
}
if ($should_autoclose) {
$this->closeTasks(
$actor,
$acting_as_phid,
$repository,
$commit,
$message,
$acting_user);
}
$data->save();
$commit->writeImportStatusFlag(
PhabricatorRepositoryCommit::IMPORTED_MESSAGE);
}
private function resolveUserPHID(
PhabricatorRepositoryCommit $commit,
$user_name) {
return id(new DiffusionResolveUserQuery())
->withCommit($commit)
->withName($user_name)
->execute();
}
private function closeTasks(
PhabricatorUser $actor,
$acting_as,
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
$message,
PhabricatorUser $acting_user = null) {
// If we we were able to identify an author for the commit, we try to act
// as that user when loading tasks marked with "Fixes Txxx". This prevents
// mistakes where a user accidentally writes the wrong task IDs and affects
// tasks they can't see (and thus can't undo the status changes for).
// This is just a guard rail, not a security measure. An attacker can still
// forge another user's identity trivially by forging author or committer
// emails. We also let commits with unrecognized authors act on any task to
// make behavior less confusing for new installs.
if (!$acting_user) {
$acting_user = $actor;
}
$maniphest = 'PhabricatorManiphestApplication';
if (!PhabricatorApplication::isClassInstalled($maniphest)) {
return;
}
$prefixes = ManiphestTaskStatus::getStatusPrefixMap();
$suffixes = ManiphestTaskStatus::getStatusSuffixMap();
$matches = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($message);
$task_statuses = array();
foreach ($matches as $match) {
$prefix = phutil_utf8_strtolower($match['prefix']);
$suffix = phutil_utf8_strtolower($match['suffix']);
$status = idx($suffixes, $suffix);
if (!$status) {
$status = idx($prefixes, $prefix);
}
foreach ($match['monograms'] as $task_monogram) {
$task_id = (int)trim($task_monogram, 'tT');
$task_statuses[$task_id] = $status;
}
}
if (!$task_statuses) {
return;
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($acting_user)
->withIDs(array_keys($task_statuses))
->needProjectPHIDs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
foreach ($tasks as $task_id => $task) {
$xactions = array();
$edge_type = ManiphestTaskHasCommitEdgeType::EDGECONST;
$edge_xaction = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'+' => array(
$commit->getPHID() => $commit->getPHID(),
),
));
$status = $task_statuses[$task_id];
if ($status) {
if ($task->getStatus() != $status) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
->setMetadataValue('commitPHID', $commit->getPHID())
->setNewValue($status);
$edge_xaction->setMetadataValue('commitPHID', $commit->getPHID());
}
}
$xactions[] = $edge_xaction;
$content_source = $this->newContentSource();
$editor = id(new ManiphestTransactionEditor())
->setActor($actor)
->setActingAsPHID($acting_as)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setUnmentionablePHIDMap(
array($commit->getPHID() => $commit->getPHID()))
->setContentSource($content_source);
$editor->applyTransactions($task, $xactions);
}
}
private function loadActingUser(PhabricatorUser $viewer, $user_phid) {
if (!$user_phid) {
return null;
}
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($user_phid) != $user_type) {
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($user_phid))
->executeOne();
if (!$user) {
return null;
}
return $user;
}
}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php
new file mode 100644
index 000000000..e81ecbe80
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php
@@ -0,0 +1,81 @@
+<?php
+
+final class PhabricatorRepositoryIdentityAssignTransaction
+ extends PhabricatorRepositoryIdentityTransactionType {
+
+ const TRANSACTIONTYPE = 'repository:identity:assign';
+
+ public function generateOldValue($object) {
+ return nonempty($object->getManuallySetUserPHID(), null);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setManuallySetUserPHID($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (!$old) {
+ return pht(
+ '%s assigned this identity to %s.',
+ $this->renderAuthor(),
+ $this->renderIdentityHandle($new));
+ } else if (!$new) {
+ return pht(
+ '%s removed %s as the assignee of this identity.',
+ $this->renderAuthor(),
+ $this->renderIdentityHandle($old));
+ } else {
+ return pht(
+ '%s changed the assigned user for this identity from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderIdentityHandle($old),
+ $this->renderIdentityHandle($new));
+ }
+ }
+
+ private function renderIdentityHandle($handle) {
+ $unassigned_token = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN;
+ if ($handle === $unassigned_token) {
+ return phutil_tag('em', array(), pht('Explicitly Unassigned'));
+ } else {
+ return $this->renderHandle($handle);
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+ $unassigned_token = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN;
+
+ foreach ($xactions as $xaction) {
+ $old = $xaction->getOldValue();
+ $new = $xaction->getNewValue();
+ if (!strlen($new)) {
+ continue;
+ }
+
+ if ($new === $old) {
+ continue;
+ }
+
+ if ($new === $unassigned_token) {
+ continue;
+ }
+
+ $assignee_list = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->getActor())
+ ->withPHIDs(array($new))
+ ->execute();
+
+ if (!$assignee_list) {
+ $errors[] = $this->newInvalidError(
+ pht('User "%s" is not a valid user.',
+ $new));
+ }
+ }
+ return $errors;
+ }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php b/src/applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php
new file mode 100644
index 000000000..54feb6c52
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorRepositoryIdentityTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
index 3d13f2c3d..335ba5885 100644
--- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
+++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
@@ -1,599 +1,642 @@
<?php
abstract class PhabricatorSearchEngineAPIMethod
extends ConduitAPIMethod {
abstract public function newSearchEngine();
final public function getQueryMaps($query) {
$maps = $this->getCustomQueryMaps($query);
// Make sure we emit empty maps as objects, not lists.
foreach ($maps as $key => $map) {
if (!$map) {
$maps[$key] = (object)$map;
}
}
if (!$maps) {
$maps = (object)$maps;
}
return $maps;
}
protected function getCustomQueryMaps($query) {
return array();
}
public function getApplication() {
$engine = $this->newSearchEngine();
$class = $engine->getApplicationClassName();
return PhabricatorApplication::getByClass($class);
}
final protected function defineParamTypes() {
return array(
'queryKey' => 'optional string',
'constraints' => 'optional map<string, wild>',
'attachments' => 'optional map<string, bool>',
'order' => 'optional order',
) + $this->getPagerParamTypes();
}
final protected function defineReturnType() {
return 'map<string, wild>';
}
final protected function execute(ConduitAPIRequest $request) {
$engine = $this->newSearchEngine()
->setViewer($request->getUser());
return $engine->buildConduitResponse($request, $this);
}
final public function getMethodDescription() {
return pht(
'This is a standard **ApplicationSearch** method which will let you '.
'list, query, or search for objects. For documentation on these '.
'endpoints, see **[[ %s | Conduit API: Using Search Endpoints ]]**.',
PhabricatorEnv::getDoclink('Conduit API: Using Search Endpoints'));
}
final public function getMethodDocumentation() {
$viewer = $this->getViewer();
$engine = $this->newSearchEngine()
->setViewer($viewer);
$query = $engine->newQuery();
$out = array();
$out[] = $this->buildQueriesBox($engine);
$out[] = $this->buildConstraintsBox($engine);
$out[] = $this->buildOrderBox($engine, $query);
$out[] = $this->buildFieldsBox($engine);
$out[] = $this->buildAttachmentsBox($engine);
$out[] = $this->buildPagingBox($engine);
return $out;
}
private function buildQueriesBox(
PhabricatorApplicationSearchEngine $engine) {
$viewer = $this->getViewer();
$info = pht(<<<EOTEXT
You can choose a builtin or saved query as a starting point for filtering
results by selecting it with `queryKey`. If you don't specify a `queryKey`,
the query will start with no constraints.
For example, many applications have builtin queries like `"active"` or
`"open"` to find only active or enabled results. To use a `queryKey`, specify
it like this:
```lang=json, name="Selecting a Builtin Query"
{
...
"queryKey": "active",
...
}
```
The table below shows the keys to use to select builtin queries and your
saved queries, but you can also use **any** query you run via the web UI as a
starting point. You can find the key for a query by examining the URI after
running a normal search.
You can use these keys to select builtin queries and your configured saved
queries:
EOTEXT
);
$named_queries = $engine->loadAllNamedQueries();
$rows = array();
foreach ($named_queries as $named_query) {
$builtin = $named_query->getIsBuiltin()
? pht('Builtin')
: pht('Custom');
$rows[] = array(
$named_query->getQueryKey(),
$named_query->getQueryName(),
$builtin,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Query Key'),
pht('Name'),
pht('Builtin'),
))
->setColumnClasses(
array(
'prewrap',
'pri wide',
null,
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Builtin and Saved Queries'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($this->buildRemarkup($info))
->appendChild($table);
}
private function buildConstraintsBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
You can apply custom constraints by passing a dictionary in `constraints`.
This will let you search for specific sets of results (for example, you may
want show only results with a certain state, status, or owner).
If you specify both a `queryKey` and `constraints`, the builtin or saved query
will be applied first as a starting point, then any additional values in
`constraints` will be applied, overwriting the defaults from the original query.
-Specify constraints like this:
+Different endpoints support different constraints. The constraints this method
+supports are detailed below. As an example, you might specify constraints like
+this:
```lang=json, name="Example Custom Constraints"
{
...
"constraints": {
"authors": ["PHID-USER-1111", "PHID-USER-2222"],
"statuses": ["open", "closed"],
...
},
...
}
```
This API endpoint supports these constraints:
EOTEXT
);
$fields = $engine->getSearchFieldsForConduit();
// As a convenience, put these fields at the very top, even if the engine
// specifies and alternate display order for the web UI. These fields are
// very important in the API and nearly useless in the web UI.
$fields = array_select_keys(
$fields,
array('ids', 'phids')) + $fields;
+ $constant_lists = array();
+
$rows = array();
foreach ($fields as $field) {
$key = $field->getConduitKey();
$label = $field->getLabel();
+ $constants = $field->newConduitConstants();
+
$type_object = $field->getConduitParameterType();
if ($type_object) {
$type = $type_object->getTypeName();
$description = $field->getDescription();
+ if ($constants) {
+ $description = array(
+ $description,
+ ' ',
+ phutil_tag('em', array(), pht('(See table below.)')),
+ );
+ }
} else {
$type = null;
$description = phutil_tag('em', array(), pht('Not supported.'));
}
$rows[] = array(
$key,
$label,
$type,
$description,
);
+
+ if ($constants) {
+ $constant_lists[] = $this->buildRemarkup(
+ pht(
+ 'Constants supported by the `%s` constraint:',
+ 'statuses'));
+
+ $constants_rows = array();
+ foreach ($constants as $constant) {
+ $constants_rows[] = array(
+ $constant->getKey(),
+ $constant->getValue(),
+ );
+ }
+
+ $constants_table = id(new AphrontTableView($constants_rows))
+ ->setHeaders(
+ array(
+ pht('Key'),
+ pht('Value'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'pre',
+ 'wide',
+ ));
+
+ $constant_lists[] = $constants_table;
+ }
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Label'),
pht('Type'),
pht('Description'),
))
->setColumnClasses(
array(
'prewrap',
'pri',
'prewrap',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Custom Query Constraints'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($this->buildRemarkup($info))
- ->appendChild($table);
+ ->appendChild($table)
+ ->appendChild($constant_lists);
}
private function buildOrderBox(
PhabricatorApplicationSearchEngine $engine,
$query) {
$orders_info = pht(<<<EOTEXT
Use `order` to choose an ordering for the results.
Either specify a single key from the builtin orders (these are a set of
meaningful, high-level, human-readable orders) or specify a custom list of
low-level columns.
To use a high-level order, choose a builtin order from the table below
and specify it like this:
```lang=json, name="Choosing a Result Order"
{
...
"order": "newest",
...
}
```
These builtin orders are available:
EOTEXT
);
$orders = $query->getBuiltinOrders();
$rows = array();
foreach ($orders as $key => $order) {
$rows[] = array(
$key,
$order['name'],
implode(', ', $order['vector']),
);
}
$orders_table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Description'),
pht('Columns'),
))
->setColumnClasses(
array(
'pri',
'',
'wide',
));
$columns_info = pht(<<<EOTEXT
You can choose a low-level column order instead. To do this, provide a list
of columns instead of a single key. This is an advanced feature.
In a custom column order:
- each column may only be specified once;
- each column may be prefixed with `-` to invert the order;
- the last column must be a unique column, usually `id`; and
- no column other than the last may be unique.
To use a low-level order, choose a sequence of columns and specify them like
this:
```lang=json, name="Using a Custom Order"
{
...
"order": ["color", "-name", "id"],
...
}
```
These low-level columns are available:
EOTEXT
);
$columns = $query->getOrderableColumns();
$rows = array();
foreach ($columns as $key => $column) {
$rows[] = array(
$key,
idx($column, 'unique') ? pht('Yes') : pht('No'),
);
}
$columns_table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Unique'),
))
->setColumnClasses(
array(
'pri',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Result Ordering'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($this->buildRemarkup($orders_info))
->appendChild($orders_table)
->appendChild($this->buildRemarkup($columns_info))
->appendChild($columns_table);
}
private function buildFieldsBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
Objects matching your query are returned as a list of dictionaries in the
`data` property of the results. Each dictionary has some metadata and a
`fields` key, which contains the information abou the object that most callers
will be interested in.
For example, the results may look something like this:
```lang=json, name="Example Results"
{
...
"data": [
{
"id": 123,
"phid": "PHID-WXYZ-1111",
"fields": {
"name": "First Example Object",
"authorPHID": "PHID-USER-2222"
}
},
{
"id": 124,
"phid": "PHID-WXYZ-3333",
"fields": {
"name": "Second Example Object",
"authorPHID": "PHID-USER-4444"
}
},
...
]
...
}
```
This result structure is standardized across all search methods, but the
available fields differ from application to application.
These are the fields available on this object type:
EOTEXT
);
$specs = $engine->getAllConduitFieldSpecifications();
$rows = array();
foreach ($specs as $key => $spec) {
$type = $spec->getType();
$description = $spec->getDescription();
$rows[] = array(
$key,
$type,
$description,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Type'),
pht('Description'),
))
->setColumnClasses(
array(
'pri',
'mono',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Object Fields'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($this->buildRemarkup($info))
->appendChild($table);
}
private function buildAttachmentsBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
By default, only basic information about objects is returned. If you want
more extensive information, you can use available `attachments` to get more
information in the results (like subscribers and projects).
Generally, requesting more information means the query executes more slowly
and returns more data (in some cases, much more data). You should normally
request only the data you need.
To request extra data, specify which attachments you want in the `attachments`
parameter:
```lang=json, name="Example Attachments Request"
{
...
"attachments": {
"subscribers": true
},
...
}
```
This example specifies that results should include information about
subscribers. In the return value, each object will now have this information
filled out in the corresponding `attachments` value:
```lang=json, name="Example Attachments Result"
{
...
"data": [
{
...
"attachments": {
"subscribers": {
"subscriberPHIDs": [
"PHID-WXYZ-2222",
],
"subscriberCount": 1,
"viewerIsSubscribed": false
}
},
...
},
...
],
...
}
```
These attachments are available:
EOTEXT
);
$attachments = $engine->getConduitSearchAttachments();
$rows = array();
foreach ($attachments as $key => $attachment) {
$rows[] = array(
$key,
$attachment->getAttachmentName(),
$attachment->getAttachmentDescription(),
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('This call does not support any attachments.'))
->setHeaders(
array(
pht('Key'),
pht('Name'),
pht('Description'),
))
->setColumnClasses(
array(
'prewrap',
'pri',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Attachments'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($this->buildRemarkup($info))
->appendChild($table);
}
private function buildPagingBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
Queries are limited to returning 100 results at a time. If you want fewer
results than this, you can use `limit` to specify a smaller limit.
If you want more results, you'll need to make additional queries to retrieve
more pages of results.
The result structure contains a `cursor` key with information you'll need in
order to fetch the next page of results. After an initial query, it will
usually look something like this:
```lang=json, name="Example Cursor Result"
{
...
"cursor": {
"limit": 100,
"after": "1234",
"before": null,
"order": null
}
...
}
```
The `limit` and `order` fields are describing the effective limit and order the
query was executed with, and are usually not of much interest. The `after` and
`before` fields give you cursors which you can pass when making another API
call in order to get the next (or previous) page of results.
To get the next page of results, repeat your API call with all the same
parameters as the original call, but pass the `after` cursor you received from
the first call in the `after` parameter when making the second call.
If you do things correctly, you should get the second page of results, and
a cursor structure like this:
```lang=json, name="Second Result Page"
{
...
"cursor": {
"limit": 5,
"after": "4567",
"before": "7890",
"order": null
}
...
}
```
You can now continue to the third page of results by passing the new `after`
cursor to the `after` parameter in your third call, or return to the previous
page of results by passing the `before` cursor to the `before` parameter. This
might be useful if you are rendering a web UI for a user and want to provide
"Next Page" and "Previous Page" links.
If `after` is `null`, there is no next page of results available. Likewise,
if `before` is `null`, there are no previous results available.
EOTEXT
);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Paging and Limits'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($this->buildRemarkup($info));
}
private function buildRemarkup($remarkup) {
$viewer = $this->getViewer();
$view = new PHUIRemarkupView($viewer, $remarkup);
$view->setRemarkupOptions(
array(
PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false,
));
return id(new PHUIBoxView())
->appendChild($view)
->addPadding(PHUI::PADDING_LARGE);
}
}
diff --git a/src/applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php b/src/applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php
index 65822b0a3..55b7c2225 100644
--- a/src/applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php
+++ b/src/applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php
@@ -1,70 +1,73 @@
<?php
final class PhabricatorFerretSearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'ferret';
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('Fulltext Search');
}
public function getExtensionOrder() {
return 1000;
}
public function supportsObject($object) {
return ($object instanceof PhabricatorFerretInterface);
}
public function applyConstraintsToQuery(
$object,
$query,
PhabricatorSavedQuery $saved,
array $map) {
if (!strlen($map['query'])) {
return;
}
$engine = $object->newFerretEngine();
$raw_query = $map['query'];
$compiler = id(new PhutilSearchQueryCompiler())
->setEnableFunctions(true);
$raw_tokens = $compiler->newTokens($raw_query);
$fulltext_tokens = array();
foreach ($raw_tokens as $raw_token) {
$fulltext_token = id(new PhabricatorFulltextToken())
->setToken($raw_token);
$fulltext_tokens[] = $fulltext_token;
}
$query->withFerretConstraint($engine, $fulltext_tokens);
}
public function getSearchFields($object) {
$fields = array();
$fields[] = id(new PhabricatorSearchTextField())
->setKey('query')
->setLabel(pht('Query'))
- ->setDescription(pht('Fulltext search.'));
+ ->setDescription(
+ pht(
+ 'Find objects matching a fulltext search query. See '.
+ '"Search User Guide" in the documentation for details.'));
return $fields;
}
public function getSearchAttachments($object) {
return array();
}
}
diff --git a/src/applications/search/field/PhabricatorSearchCheckboxesField.php b/src/applications/search/field/PhabricatorSearchCheckboxesField.php
index e1a72f457..0985c976c 100644
--- a/src/applications/search/field/PhabricatorSearchCheckboxesField.php
+++ b/src/applications/search/field/PhabricatorSearchCheckboxesField.php
@@ -1,52 +1,64 @@
<?php
final class PhabricatorSearchCheckboxesField
extends PhabricatorSearchField {
private $options;
public function setOptions(array $options) {
$this->options = $options;
return $this;
}
public function getOptions() {
return $this->options;
}
protected function getDefaultValue() {
return array();
}
protected function didReadValueFromSavedQuery($value) {
if (!is_array($value)) {
return array();
}
return $value;
}
protected function getValueFromRequest(AphrontRequest $request, $key) {
return $this->getListFromRequest($request, $key);
}
protected function newControl() {
$value = array_fuse($this->getValue());
$control = new AphrontFormCheckboxControl();
foreach ($this->getOptions() as $key => $option) {
$control->addCheckbox(
$this->getKey().'[]',
$key,
$option,
isset($value[$key]));
}
return $control;
}
protected function newConduitParameterType() {
return new ConduitStringListParameterType();
}
+ public function newConduitConstants() {
+ $list = array();
+
+ foreach ($this->getOptions() as $key => $option) {
+ $list[] = id(new ConduitConstantDescription())
+ ->setKey($key)
+ ->setValue($option);
+ }
+
+ return $list;
+ }
+
}
diff --git a/src/applications/search/field/PhabricatorSearchField.php b/src/applications/search/field/PhabricatorSearchField.php
index 1a038ec66..36db0523b 100644
--- a/src/applications/search/field/PhabricatorSearchField.php
+++ b/src/applications/search/field/PhabricatorSearchField.php
@@ -1,422 +1,426 @@
<?php
/**
* @task config Configuring Fields
* @task error Handling Errors
* @task io Reading and Writing Field Values
* @task conduit Integration with Conduit
* @task util Utility Methods
*/
abstract class PhabricatorSearchField extends Phobject {
private $key;
private $conduitKey;
private $viewer;
private $value;
private $label;
private $aliases = array();
private $errors = array();
private $description;
private $isHidden;
private $enableForConduit = true;
/* -( Configuring Fields )------------------------------------------------- */
/**
* Set the primary key for the field, like `projectPHIDs`.
*
* You can set human-readable aliases with @{method:setAliases}.
*
* The key should be a short, unique (within a search engine) string which
* does not contain any special characters.
*
* @param string Unique key which identifies the field.
* @return this
* @task config
*/
public function setKey($key) {
$this->key = $key;
return $this;
}
/**
* Get the field's key.
*
* @return string Unique key for this field.
* @task config
*/
public function getKey() {
return $this->key;
}
/**
* Set a human-readable label for the field.
*
* This should be a short text string, like "Reviewers" or "Colors".
*
* @param string Short, human-readable field label.
* @return this
* task config
*/
public function setLabel($label) {
$this->label = $label;
return $this;
}
/**
* Get the field's human-readable label.
*
* @return string Short, human-readable field label.
* @task config
*/
public function getLabel() {
return $this->label;
}
/**
* Set the acting viewer.
*
* Engines do not need to do this explicitly; it will be done on their
* behalf by the caller.
*
* @param PhabricatorUser Viewer.
* @return this
* @task config
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Get the acting viewer.
*
* @return PhabricatorUser Viewer.
* @task config
*/
public function getViewer() {
return $this->viewer;
}
/**
* Provide alternate field aliases, usually more human-readable versions
* of the key.
*
* These aliases can be used when building GET requests, so you can provide
* an alias like `authors` to let users write `&authors=alincoln` instead of
* `&authorPHIDs=alincoln`. This is a little easier to use.
*
* @param list<string> List of aliases for this field.
* @return this
* @task config
*/
public function setAliases(array $aliases) {
$this->aliases = $aliases;
return $this;
}
/**
* Get aliases for this field.
*
* @return list<string> List of aliases for this field.
* @task config
*/
public function getAliases() {
return $this->aliases;
}
/**
* Provide an alternate field key for Conduit.
*
* This can allow you to choose a more usable key for API endpoints.
* If no key is provided, the main key is used.
*
* @param string Alternate key for Conduit.
* @return this
* @task config
*/
public function setConduitKey($conduit_key) {
$this->conduitKey = $conduit_key;
return $this;
}
/**
* Get the field key for use in Conduit.
*
* @return string Conduit key for this field.
* @task config
*/
public function getConduitKey() {
if ($this->conduitKey !== null) {
return $this->conduitKey;
}
return $this->getKey();
}
/**
* Set a human-readable description for this field.
*
* @param string Human-readable description.
* @return this
* @task config
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
/**
* Get this field's human-readable description.
*
* @return string|null Human-readable description.
* @task config
*/
public function getDescription() {
return $this->description;
}
/**
* Hide this field from the web UI.
*
* @param bool True to hide the field from the web UI.
* @return this
* @task config
*/
public function setIsHidden($is_hidden) {
$this->isHidden = $is_hidden;
return $this;
}
/**
* Should this field be hidden from the web UI?
*
* @return bool True to hide the field in the web UI.
* @task config
*/
public function getIsHidden() {
return $this->isHidden;
}
/* -( Handling Errors )---------------------------------------------------- */
protected function addError($short, $long) {
$this->errors[] = array($short, $long);
return $this;
}
public function getErrors() {
return $this->errors;
}
protected function validateControlValue($value) {
return;
}
protected function getShortError() {
$error = head($this->getErrors());
if ($error) {
return head($error);
}
return null;
}
/* -( Reading and Writing Field Values )----------------------------------- */
public function readValueFromRequest(AphrontRequest $request) {
$check = array_merge(array($this->getKey()), $this->getAliases());
foreach ($check as $key) {
if ($this->getValueExistsInRequest($request, $key)) {
return $this->getValueFromRequest($request, $key);
}
}
return $this->getDefaultValue();
}
protected function getValueExistsInRequest(AphrontRequest $request, $key) {
return $request->getExists($key);
}
abstract protected function getValueFromRequest(
AphrontRequest $request,
$key);
public function readValueFromSavedQuery(PhabricatorSavedQuery $saved) {
$value = $saved->getParameter(
$this->getKey(),
$this->getDefaultValue());
$this->value = $this->didReadValueFromSavedQuery($value);
$this->validateControlValue($value);
return $this;
}
protected function didReadValueFromSavedQuery($value) {
return $value;
}
public function getValue() {
return $this->value;
}
protected function getValueForControl() {
return $this->value;
}
protected function getDefaultValue() {
return null;
}
public function getValueForQuery($value) {
return $value;
}
/* -( Rendering Controls )------------------------------------------------- */
protected function newControl() {
throw new PhutilMethodNotImplementedException();
}
protected function renderControl() {
if ($this->getIsHidden()) {
return null;
}
$control = $this->newControl();
if (!$control) {
return null;
}
// TODO: We should `setError($this->getShortError())` here, but it looks
// terrible in the form layout.
return $control
->setValue($this->getValueForControl())
->setName($this->getKey())
->setLabel($this->getLabel());
}
public function appendToForm(AphrontFormView $form) {
$control = $this->renderControl();
if ($control !== null) {
$form->appendControl($this->renderControl());
}
return $this;
}
/* -( Integration with Conduit )------------------------------------------- */
/**
* @task conduit
*/
final public function getConduitParameterType() {
if (!$this->getEnableForConduit()) {
return false;
}
$type = $this->newConduitParameterType();
if ($type) {
$type->setViewer($this->getViewer());
}
return $type;
}
protected function newConduitParameterType() {
return null;
}
public function getValueExistsInConduitRequest(array $constraints) {
return $this->getConduitParameterType()->getExists(
$constraints,
$this->getConduitKey());
}
public function readValueFromConduitRequest(
array $constraints,
$strict = true) {
return $this->getConduitParameterType()->getValue(
$constraints,
$this->getConduitKey(),
$strict);
}
public function getValidConstraintKeys() {
return $this->getConduitParameterType()->getKeys(
$this->getConduitKey());
}
final public function setEnableForConduit($enable) {
$this->enableForConduit = $enable;
return $this;
}
final public function getEnableForConduit() {
return $this->enableForConduit;
}
+ public function newConduitConstants() {
+ return array();
+ }
+
/* -( Utility Methods )----------------------------------------------------- */
/**
* Read a list of items from the request, in either array format or string
* format:
*
* list[]=item1&list[]=item2
* list=item1,item2
*
* This provides flexibility when constructing URIs, especially from external
* sources.
*
* @param AphrontRequest Request to read strings from.
* @param string Key to read in the request.
* @return list<string> List of values.
* @task utility
*/
protected function getListFromRequest(
AphrontRequest $request,
$key) {
$list = $request->getArr($key, null);
if ($list === null) {
$list = $request->getStrList($key);
}
if (!$list) {
return array();
}
return $list;
}
}
diff --git a/src/applications/search/field/PhabricatorSearchSubscribersField.php b/src/applications/search/field/PhabricatorSearchSubscribersField.php
index 7102f3391..f3cf2094e 100644
--- a/src/applications/search/field/PhabricatorSearchSubscribersField.php
+++ b/src/applications/search/field/PhabricatorSearchSubscribersField.php
@@ -1,27 +1,28 @@
<?php
final class PhabricatorSearchSubscribersField
extends PhabricatorSearchTokenizerField {
protected function getDefaultValue() {
return array();
}
protected function getValueFromRequest(AphrontRequest $request, $key) {
$allow_types = array(
PhabricatorProjectProjectPHIDType::TYPECONST,
+ PhabricatorOwnersPackagePHIDType::TYPECONST,
);
return $this->getUsersFromRequest($request, $key, $allow_types);
}
protected function newDatasource() {
return new PhabricatorMetaMTAMailableFunctionDatasource();
}
protected function newConduitParameterType() {
// TODO: Ideally, this should eventually be a "Subscribers" type which
// accepts projects as well.
return new ConduitUserListParameterType();
}
}
diff --git a/src/applications/search/query/PhabricatorFulltextToken.php b/src/applications/search/query/PhabricatorFulltextToken.php
index 8dc2cee3c..e99c2151c 100644
--- a/src/applications/search/query/PhabricatorFulltextToken.php
+++ b/src/applications/search/query/PhabricatorFulltextToken.php
@@ -1,92 +1,96 @@
<?php
final class PhabricatorFulltextToken extends Phobject {
private $token;
private $isShort;
private $isStopword;
public function setToken(PhutilSearchQueryToken $token) {
$this->token = $token;
return $this;
}
public function getToken() {
return $this->token;
}
public function isQueryable() {
return !$this->getIsShort() && !$this->getIsStopword();
}
public function setIsShort($is_short) {
$this->isShort = $is_short;
return $this;
}
public function getIsShort() {
return $this->isShort;
}
public function setIsStopword($is_stopword) {
$this->isStopword = $is_stopword;
return $this;
}
public function getIsStopword() {
return $this->isStopword;
}
public function newTag() {
$token = $this->getToken();
$tip = null;
$icon = null;
if ($this->getIsShort()) {
$shade = PHUITagView::COLOR_GREY;
$tip = pht('Ignored Short Word');
} else if ($this->getIsStopword()) {
$shade = PHUITagView::COLOR_GREY;
$tip = pht('Ignored Common Word');
} else {
$operator = $token->getOperator();
switch ($operator) {
case PhutilSearchQueryCompiler::OPERATOR_NOT:
$shade = PHUITagView::COLOR_RED;
$icon = 'fa-minus';
break;
case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
$tip = pht('Substring Search');
$shade = PHUITagView::COLOR_VIOLET;
break;
+ case PhutilSearchQueryCompiler::OPERATOR_EXACT:
+ $tip = pht('Exact Search');
+ $shade = PHUITagView::COLOR_GREEN;
+ break;
default:
$shade = PHUITagView::COLOR_BLUE;
break;
}
}
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor($shade)
->setName($token->getValue());
if ($tip !== null) {
Javelin::initBehavior('phabricator-tooltips');
$tag
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $tip,
));
}
if ($icon !== null) {
$tag->setIcon($icon);
}
return $tag;
}
}
diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php
index 8819f4186..6c527733e 100644
--- a/src/applications/search/view/PhabricatorSearchResultView.php
+++ b/src/applications/search/view/PhabricatorSearchResultView.php
@@ -1,185 +1,186 @@
<?php
final class PhabricatorSearchResultView extends AphrontView {
private $handle;
private $object;
private $tokens;
public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
public function setTokens(array $tokens) {
assert_instances_of($tokens, 'PhabricatorFulltextToken');
$this->tokens = $tokens;
return $this;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function render() {
$handle = $this->handle;
if (!$handle->isComplete()) {
return;
}
require_celerity_resource('phabricator-search-results-css');
$type_name = nonempty($handle->getTypeName(), pht('Document'));
$raw_title = $handle->getFullName();
$title = $this->emboldenQuery($raw_title);
$item = id(new PHUIObjectItemView())
->setHeader($title)
->setTitleText($raw_title)
->setHref($handle->getURI())
->setImageURI($handle->getImageURI())
->addAttribute($type_name);
if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
$item->setDisabled(true);
$item->addAttribute(pht('Closed'));
}
return $item;
}
/**
* Find the words which are part of the query string, and bold them in a
* result string. This makes it easier for users to see why a result
* matched their query.
*/
private function emboldenQuery($str) {
$tokens = $this->tokens;
if (!$tokens) {
return $str;
}
if (count($tokens) > 16) {
return $str;
}
if (!strlen($str)) {
return $str;
}
if (strlen($str) > 2048) {
return $str;
}
$patterns = array();
foreach ($tokens as $token) {
$raw_token = $token->getToken();
$operator = $raw_token->getOperator();
$value = $raw_token->getValue();
switch ($operator) {
case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
+ case PhutilSearchQueryCompiler::OPERATOR_EXACT:
$patterns[] = '(('.preg_quote($value).'))ui';
break;
case PhutilSearchQueryCompiler::OPERATOR_AND:
$patterns[] = '((?<=\W|^)('.preg_quote($value).')(?=\W|\z))ui';
break;
default:
// Don't highlight anything else, particularly "NOT".
break;
}
}
// Find all matches for all query terms in the document title, then reduce
// them to a map from offsets to highlighted sequence lengths. If two terms
// match at the same position, we choose the longer one.
$all_matches = array();
foreach ($patterns as $pattern) {
$matches = null;
$ok = preg_match_all(
$pattern,
$str,
$matches,
PREG_OFFSET_CAPTURE);
if (!$ok) {
continue;
}
foreach ($matches[1] as $match) {
$match_text = $match[0];
$match_offset = $match[1];
if (!isset($all_matches[$match_offset])) {
$all_matches[$match_offset] = 0;
}
$all_matches[$match_offset] = max(
$all_matches[$match_offset],
strlen($match_text));
}
}
// Go through the string one display glyph at a time. If a glyph starts
// on a highlighted byte position, turn on highlighting for the nubmer
// of matching bytes. If a query searches for "e" and the document contains
// an "e" followed by a bunch of combining marks, this will correctly
// highlight the entire glyph.
$parts = array();
$highlight = 0;
$offset = 0;
foreach (phutil_utf8v_combined($str) as $character) {
$length = strlen($character);
if (isset($all_matches[$offset])) {
$highlight = $all_matches[$offset];
}
if ($highlight > 0) {
$is_highlighted = true;
$highlight -= $length;
} else {
$is_highlighted = false;
}
$parts[] = array(
'text' => $character,
'highlighted' => $is_highlighted,
);
$offset += $length;
}
// Combine all the sequences together so we aren't emitting a tag around
// every individual character.
$last = null;
foreach ($parts as $key => $part) {
if ($last !== null) {
if ($part['highlighted'] == $parts[$last]['highlighted']) {
$parts[$last]['text'] .= $part['text'];
unset($parts[$key]);
continue;
}
}
$last = $key;
}
// Finally, add tags.
$result = array();
foreach ($parts as $part) {
if ($part['highlighted']) {
$result[] = phutil_tag('strong', array(), $part['text']);
} else {
$result[] = $part['text'];
}
}
return $result;
}
}
diff --git a/src/applications/search/worker/PhabricatorSearchWorker.php b/src/applications/search/worker/PhabricatorSearchWorker.php
index 607baed69..f93df6398 100644
--- a/src/applications/search/worker/PhabricatorSearchWorker.php
+++ b/src/applications/search/worker/PhabricatorSearchWorker.php
@@ -1,85 +1,94 @@
<?php
final class PhabricatorSearchWorker extends PhabricatorWorker {
public static function queueDocumentForIndexing($phid, $parameters = null) {
if ($parameters === null) {
$parameters = array();
}
parent::scheduleTask(
__CLASS__,
array(
'documentPHID' => $phid,
'parameters' => $parameters,
),
array(
'priority' => parent::PRIORITY_IMPORT,
+ 'objectPHID' => $phid,
));
}
protected function doWork() {
$data = $this->getTaskData();
$object_phid = idx($data, 'documentPHID');
$object = $this->loadObjectForIndexing($object_phid);
$engine = id(new PhabricatorIndexEngine())
->setObject($object);
$parameters = idx($data, 'parameters', array());
$engine->setParameters($parameters);
if (!$engine->shouldIndexObject()) {
return;
}
$key = "index.{$object_phid}";
$lock = PhabricatorGlobalLock::newLock($key);
- $lock->lock(1);
+ try {
+ $lock->lock(1);
+ } catch (PhutilLockException $ex) {
+ // If we fail to acquire the lock, just yield. It's expected that we may
+ // contend on this lock occasionally if a large object receives many
+ // updates in a short period of time, and it's appropriate to just retry
+ // rebuilding the index later.
+ throw new PhabricatorWorkerYieldException(15);
+ }
try {
// Reload the object now that we have a lock, to make sure we have the
// most current version.
$object = $this->loadObjectForIndexing($object->getPHID());
$engine->setObject($object);
$engine->indexObject();
} catch (Exception $ex) {
$lock->unlock();
if (!($ex instanceof PhabricatorWorkerPermanentFailureException)) {
$ex = new PhabricatorWorkerPermanentFailureException(
pht(
'Failed to update search index for document "%s": %s',
$object_phid,
$ex->getMessage()));
}
throw $ex;
}
$lock->unlock();
}
private function loadObjectForIndexing($phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Unable to load object "%s" to rebuild indexes.',
$phid));
}
return $object;
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
index 1369643ff..e29d5e380 100644
--- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
@@ -1,92 +1,90 @@
<?php
final class PhabricatorSubscriptionsMuteController
extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!($object instanceof PhabricatorSubscribableInterface)) {
return new Aphront400Response();
}
$muted_type = PhabricatorMutedByEdgeType::EDGECONST;
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($object->getPHID()))
->withEdgeTypes(array($muted_type))
->withDestinationPHIDs(array($viewer->getPHID()));
$edge_query->execute();
$is_mute = !$edge_query->getDestinationPHIDs();
$object_uri = $handle->getURI();
if ($request->isFormPost()) {
if ($is_mute) {
$xaction_value = array(
'+' => array_fuse(array($viewer->getPHID())),
);
} else {
$xaction_value = array(
'-' => array_fuse(array($viewer->getPHID())),
);
}
- $muted_type = PhabricatorMutedByEdgeType::EDGECONST;
-
$xaction = id($object->getApplicationTransactionTemplate())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $muted_type)
->setNewValue($xaction_value);
$editor = id($object->getApplicationTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions(
$object->getApplicationTransactionObject(),
array($xaction));
return id(new AphrontReloadResponse())->setURI($object_uri);
}
$dialog = $this->newDialog()
->addCancelButton($object_uri);
if ($is_mute) {
$dialog
->setTitle(pht('Mute Notifications'))
->appendParagraph(
pht(
'Mute this object? You will no longer receive notifications or '.
'email about it.'))
->addSubmitButton(pht('Mute'));
} else {
$dialog
->setTitle(pht('Unmute Notifications'))
->appendParagraph(
pht(
'Unmute this object? You will receive notifications and email '.
'again.'))
->addSubmitButton(pht('Unmute'));
}
return $dialog;
}
}
diff --git a/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php b/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php
index 7d1b8d799..da157848e 100644
--- a/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php
+++ b/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php
@@ -1,81 +1,81 @@
<?php
final class SubscriptionListDialogBuilder extends Phobject {
private $viewer;
private $handles;
private $objectPHID;
private $title;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function getHandles() {
return $this->handles;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function buildDialog() {
$phid = $this->getObjectPHID();
$handles = $this->getHandles();
$object_handle = $handles[$phid];
unset($handles[$phid]);
return id(new AphrontDialogView())
->setUser($this->getViewer())
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($this->getTitle())
->setObjectList($this->buildBody($this->getViewer(), $handles))
->addCancelButton($object_handle->getURI(), pht('Close'));
}
- private function buildBody(PhabricatorUser $viewer, array $handles) {
+ private function buildBody(PhabricatorUser $viewer, $handles) {
$list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($handles as $handle) {
$item = id(new PHUIObjectItemView())
->setHeader($handle->getFullName())
->setHref($handle->getURI())
->setDisabled($handle->isDisabled());
if ($handle->getImageURI()) {
$item->setImageURI($handle->getImageURI());
}
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php
index 0ec8f6a7a..3fa40b391 100644
--- a/src/applications/system/application/PhabricatorSystemApplication.php
+++ b/src/applications/system/application/PhabricatorSystemApplication.php
@@ -1,32 +1,33 @@
<?php
final class PhabricatorSystemApplication extends PhabricatorApplication {
public function getName() {
return pht('System');
}
public function canUninstall() {
return false;
}
public function isUnlisted() {
return true;
}
public function getRoutes() {
return array(
'/status/' => 'PhabricatorStatusController',
'/debug/' => 'PhabricatorDebugController',
'/robots.txt' => 'PhabricatorRobotsController',
'/services/' => array(
'encoding/' => 'PhabricatorSystemSelectEncodingController',
'highlight/' => 'PhabricatorSystemSelectHighlightController',
),
'/readonly/' => array(
'(?P<reason>[^/]+)/' => 'PhabricatorSystemReadOnlyController',
),
+ '/object/(?P<name>[^/]+)/' => 'PhabricatorSystemObjectController',
);
}
}
diff --git a/src/applications/system/controller/PhabricatorSystemObjectController.php b/src/applications/system/controller/PhabricatorSystemObjectController.php
new file mode 100644
index 000000000..464c36a59
--- /dev/null
+++ b/src/applications/system/controller/PhabricatorSystemObjectController.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhabricatorSystemObjectController
+ extends PhabricatorController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+ $name = $request->getURIData('name');
+
+ $object = id(new PhabricatorObjectQuery())
+ ->setViewer($viewer)
+ ->withNames(array($name))
+ ->executeOne();
+ if (!$object) {
+ return new Aphront404Response();
+ }
+
+ $phid = $object->getPHID();
+ $handles = $viewer->loadHandles(array($phid));
+ $handle = $handles[$phid];
+
+ $object_uri = $handle->getURI();
+ if (!strlen($object_uri)) {
+ return $this->newDialog()
+ ->setTitle(pht('No Object URI'))
+ ->appendParagraph(
+ pht(
+ 'Object "%s" exists, but does not have a URI to redirect to.',
+ $name))
+ ->addCancelButton('/', pht('Done'));
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($object_uri);
+ }
+}
diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
index 66285547b..0394432ff 100644
--- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
+++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
@@ -1,230 +1,242 @@
<?php
final class TransactionSearchConduitAPIMethod
extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'transaction.search';
}
public function getMethodDescription() {
return pht('Read transactions for an object.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return pht('This method is new and experimental.');
}
protected function defineParamTypes() {
return array(
'objectIdentifier' => 'phid|string',
'constraints' => 'map<string, wild>',
) + $this->getPagerParamTypes();
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$pager = $this->newPager($request);
$object_name = $request->getValue('objectIdentifier', null);
if (!strlen($object_name)) {
throw new Exception(
pht(
'When calling "transaction.search", you must provide an object to '.
'retrieve transactions for.'));
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($object_name))
->executeOne();
if (!$object) {
throw new Exception(
pht(
'No object "%s" exists.',
$object_name));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Object "%s" does not implement "%s", so transactions can not '.
'be loaded for it.'));
}
$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
$object);
$xaction_query
->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer);
$constraints = $request->getValue('constraints', array());
PhutilTypeSpec::checkMap(
$constraints,
array(
'phids' => 'optional list<string>',
));
$with_phids = idx($constraints, 'phids');
+
+ if ($with_phids === array()) {
+ throw new Exception(
+ pht(
+ 'Constraint "phids" to "transaction.search" requires nonempty list, '.
+ 'empty list provided.'));
+ }
+
if ($with_phids) {
$xaction_query->withPHIDs($with_phids);
}
$xactions = $xaction_query->executeWithCursorPager($pager);
+ $comment_map = array();
if ($xactions) {
$template = head($xactions)->getApplicationTransactionCommentObject();
+ if ($template) {
- $query = new PhabricatorApplicationTransactionTemplatedCommentQuery();
+ $query = new PhabricatorApplicationTransactionTemplatedCommentQuery();
- $comment_map = $query
- ->setViewer($viewer)
- ->setTemplate($template)
- ->withTransactionPHIDs(mpull($xactions, 'getPHID'))
- ->execute();
-
- $comment_map = msort($comment_map, 'getCommentVersion');
- $comment_map = array_reverse($comment_map);
- $comment_map = mgroup($comment_map, 'getTransactionPHID');
- } else {
- $comment_map = array();
+ $comment_map = $query
+ ->setViewer($viewer)
+ ->setTemplate($template)
+ ->withTransactionPHIDs(mpull($xactions, 'getPHID'))
+ ->execute();
+
+ $comment_map = msort($comment_map, 'getCommentVersion');
+ $comment_map = array_reverse($comment_map);
+ $comment_map = mgroup($comment_map, 'getTransactionPHID');
+ }
}
$modular_classes = array();
$modular_objects = array();
$modular_xactions = array();
foreach ($xactions as $xaction) {
if (!$xaction instanceof PhabricatorModularTransaction) {
continue;
}
// TODO: Hack things so certain transactions which don't have a modular
// type yet can use a pseudotype until they modularize. Some day, we'll
// modularize everything and remove this.
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$modular_template = new DifferentialRevisionInlineTransaction();
break;
default:
$modular_template = $xaction->getModularType();
break;
}
$modular_class = get_class($modular_template);
if (!isset($modular_objects[$modular_class])) {
try {
$modular_object = newv($modular_class, array());
$modular_objects[$modular_class] = $modular_object;
} catch (Exception $ex) {
continue;
}
}
$modular_classes[$xaction->getPHID()] = $modular_class;
$modular_xactions[$modular_class][] = $xaction;
}
$modular_data_map = array();
foreach ($modular_objects as $class => $modular_type) {
$modular_data_map[$class] = $modular_type
->setViewer($viewer)
->loadTransactionTypeConduitData($modular_xactions[$class]);
}
$data = array();
foreach ($xactions as $xaction) {
$comments = idx($comment_map, $xaction->getPHID());
$comment_data = array();
if ($comments) {
$removed = head($comments)->getIsDeleted();
foreach ($comments as $comment) {
if ($removed) {
// If the most recent version of the comment has been removed,
// don't show the history. This is for consistency with the web
// UI, which also prevents users from retrieving the content of
// removed comments.
$content = array(
'raw' => '',
);
} else {
$content = array(
'raw' => (string)$comment->getContent(),
);
}
$comment_data[] = array(
'id' => (int)$comment->getID(),
'phid' => (string)$comment->getPHID(),
'version' => (int)$comment->getCommentVersion(),
'authorPHID' => (string)$comment->getAuthorPHID(),
'dateCreated' => (int)$comment->getDateCreated(),
'dateModified' => (int)$comment->getDateModified(),
'removed' => (bool)$comment->getIsDeleted(),
'content' => $content,
);
}
}
$fields = array();
$type = null;
if (isset($modular_classes[$xaction->getPHID()])) {
$modular_class = $modular_classes[$xaction->getPHID()];
$modular_object = $modular_objects[$modular_class];
$modular_data = $modular_data_map[$modular_class];
$type = $modular_object->getTransactionTypeForConduit($xaction);
$fields = $modular_object->getFieldValuesForConduit(
$xaction,
$modular_data);
}
if (!$fields) {
$fields = (object)$fields;
}
// If we haven't found a modular type, fallback for some simple core
// types. Ideally, we'll modularize everything some day.
if ($type === null) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$type = 'comment';
break;
+ case PhabricatorTransactions::TYPE_CREATE:
+ $type = 'create';
+ break;
}
}
$data[] = array(
'id' => (int)$xaction->getID(),
'phid' => (string)$xaction->getPHID(),
'type' => $type,
'authorPHID' => (string)$xaction->getAuthorPHID(),
'objectPHID' => (string)$xaction->getObjectPHID(),
'dateCreated' => (int)$xaction->getDateCreated(),
'dateModified' => (int)$xaction->getDateModified(),
'comments' => $comment_data,
'fields' => $fields,
);
}
$results = array(
'data' => $data,
);
return $this->addPagerResults($results, $pager);
}
}
diff --git a/src/applications/transactions/constants/PhabricatorTransactions.php b/src/applications/transactions/constants/PhabricatorTransactions.php
index 4089187ca..7a606fe65 100644
--- a/src/applications/transactions/constants/PhabricatorTransactions.php
+++ b/src/applications/transactions/constants/PhabricatorTransactions.php
@@ -1,40 +1,41 @@
<?php
final class PhabricatorTransactions extends Phobject {
const TYPE_COMMENT = 'core:comment';
const TYPE_SUBSCRIBERS = 'core:subscribers';
const TYPE_VIEW_POLICY = 'core:view-policy';
const TYPE_EDIT_POLICY = 'core:edit-policy';
const TYPE_JOIN_POLICY = 'core:join-policy';
const TYPE_EDGE = 'core:edge';
const TYPE_CUSTOMFIELD = 'core:customfield';
const TYPE_TOKEN = 'token:give';
const TYPE_INLINESTATE = 'core:inlinestate';
const TYPE_SPACE = 'core:space';
const TYPE_CREATE = 'core:create';
const TYPE_COLUMNS = 'core:columns';
const TYPE_SUBTYPE = 'core:subtype';
+ const TYPE_HISTORY = 'core:history';
const COLOR_RED = 'red';
const COLOR_ORANGE = 'orange';
const COLOR_YELLOW = 'yellow';
const COLOR_GREEN = 'green';
const COLOR_SKY = 'sky';
const COLOR_BLUE = 'blue';
const COLOR_INDIGO = 'indigo';
const COLOR_VIOLET = 'violet';
const COLOR_GREY = 'grey';
const COLOR_BLACK = 'black';
public static function getInlineStateMap() {
return array(
PhabricatorInlineCommentInterface::STATE_DRAFT =>
PhabricatorInlineCommentInterface::STATE_DONE,
PhabricatorInlineCommentInterface::STATE_UNDRAFT =>
PhabricatorInlineCommentInterface::STATE_UNDONE,
);
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index 84160a3c1..2029fde5b 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2612 +1,2624 @@
<?php
/**
* @task fields Managing Fields
* @task text Display Text
* @task config Edit Engine Configuration
* @task uri Managing URIs
* @task load Creating and Loading Objects
* @task web Responding to Web Requests
* @task edit Responding to Edit Requests
* @task http Responding to HTTP Parameter Requests
* @task conduit Responding to Conduit Requests
*/
abstract class PhabricatorEditEngine
extends Phobject
implements PhabricatorPolicyInterface {
const EDITENGINECONFIG_DEFAULT = 'default';
const SUBTYPE_DEFAULT = 'default';
private $viewer;
private $controller;
private $isCreate;
private $editEngineConfiguration;
private $contextParameters = array();
private $targetObject;
private $page;
private $pages;
private $navigation;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
$this->setViewer($controller->getViewer());
return $this;
}
final public function getController() {
return $this->controller;
}
final public function getEngineKey() {
$key = $this->getPhobjectClassConstant('ENGINECONST', 64);
if (strpos($key, '/') !== false) {
throw new Exception(
pht(
'EditEngine ("%s") contains an invalid key character "/".',
get_class($this)));
}
return $key;
}
final public function getApplication() {
$app_class = $this->getEngineApplicationClass();
return PhabricatorApplication::getByClass($app_class);
}
final public function addContextParameter($key) {
$this->contextParameters[] = $key;
return $this;
}
public function isEngineConfigurable() {
return true;
}
public function isEngineExtensible() {
return true;
}
public function isDefaultQuickCreateEngine() {
return false;
}
public function getDefaultQuickCreateFormKeys() {
$keys = array();
if ($this->isDefaultQuickCreateEngine()) {
$keys[] = self::EDITENGINECONFIG_DEFAULT;
}
foreach ($keys as $idx => $key) {
$keys[$idx] = $this->getEngineKey().'/'.$key;
}
return $keys;
}
public static function splitFullKey($full_key) {
return explode('/', $full_key, 2);
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())
->addString($this->getObjectCreateShortText());
}
/**
* Force the engine to edit a particular object.
*/
public function setTargetObject($target_object) {
$this->targetObject = $target_object;
return $this;
}
public function getTargetObject() {
return $this->targetObject;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
/* -( Managing Fields )---------------------------------------------------- */
abstract public function getEngineApplicationClass();
abstract protected function buildCustomEditFields($object);
public function getFieldsForConfig(
PhabricatorEditEngineConfiguration $config) {
$object = $this->newEditableObject();
$this->editEngineConfiguration = $config;
// This is mostly making sure that we fill in default values.
$this->setIsCreate(true);
return $this->buildEditFields($object);
}
final protected function buildEditFields($object) {
$viewer = $this->getViewer();
$fields = $this->buildCustomEditFields($object);
foreach ($fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
}
$fields = mpull($fields, null, 'getKey');
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension->setViewer($viewer);
if (!$extension->supportsObject($this, $object)) {
continue;
}
$extension_fields = $extension->buildCustomEditFields($this, $object);
// TODO: Validate this in more detail with a more tailored error.
assert_instances_of($extension_fields, 'PhabricatorEditField');
foreach ($extension_fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
$group_key = $field->getBulkEditGroupKey();
if ($group_key === null) {
$field->setBulkEditGroupKey('extension');
}
}
$extension_fields = mpull($extension_fields, null, 'getKey');
foreach ($extension_fields as $key => $field) {
$fields[$key] = $field;
}
}
$config = $this->getEditEngineConfiguration();
$fields = $this->willConfigureFields($object, $fields);
$fields = $config->applyConfigurationToFields($this, $object, $fields);
$fields = $this->applyPageToFields($object, $fields);
return $fields;
}
protected function willConfigureFields($object, array $fields) {
return $fields;
}
final public function supportsSubtypes() {
try {
$object = $this->newEditableObject();
} catch (Exception $ex) {
return false;
}
return ($object instanceof PhabricatorEditEngineSubtypeInterface);
}
final public function newSubtypeMap() {
return $this->newEditableObject()->newEditEngineSubtypeMap();
}
/* -( Display Text )------------------------------------------------------- */
/**
* @task text
*/
abstract public function getEngineName();
/**
* @task text
*/
abstract protected function getObjectCreateTitleText($object);
/**
* @task text
*/
protected function getFormHeaderText($object) {
$config = $this->getEditEngineConfiguration();
return $config->getName();
}
/**
* @task text
*/
abstract protected function getObjectEditTitleText($object);
/**
* @task text
*/
abstract protected function getObjectCreateShortText();
/**
* @task text
*/
abstract protected function getObjectName();
/**
* @task text
*/
abstract protected function getObjectEditShortText($object);
/**
* @task text
*/
protected function getObjectCreateButtonText($object) {
return $this->getObjectCreateTitleText($object);
}
/**
* @task text
*/
protected function getObjectEditButtonText($object) {
return pht('Save Changes');
}
/**
* @task text
*/
protected function getCommentViewSeriousHeaderText($object) {
return pht('Take Action');
}
/**
* @task text
*/
protected function getCommentViewSeriousButtonText($object) {
return pht('Submit');
}
/**
* @task text
*/
protected function getCommentViewHeaderText($object) {
return $this->getCommentViewSeriousHeaderText($object);
}
/**
* @task text
*/
protected function getCommentViewButtonText($object) {
return $this->getCommentViewSeriousButtonText($object);
}
/**
* @task text
*/
protected function getPageHeader($object) {
return null;
}
/**
* Return a human-readable header describing what this engine is used to do,
* like "Configure Maniphest Task Forms".
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryHeader();
/**
* Return a human-readable summary of what this engine is used to do.
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryText();
/* -( Edit Engine Configuration )------------------------------------------ */
protected function supportsEditEngineConfiguration() {
return true;
}
final protected function getEditEngineConfiguration() {
return $this->editEngineConfiguration;
}
private function newConfigurationQuery() {
return id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($this->getViewer())
->withEngineKeys(array($this->getEngineKey()));
}
private function loadEditEngineConfigurationWithQuery(
PhabricatorEditEngineConfigurationQuery $query,
$sort_method) {
if ($sort_method) {
$results = $query->execute();
$results = msort($results, $sort_method);
$result = head($results);
} else {
$result = $query->executeOne();
}
if (!$result) {
return null;
}
$this->editEngineConfiguration = $result;
return $result;
}
private function loadEditEngineConfigurationWithIdentifier($identifier) {
$query = $this->newConfigurationQuery()
->withIdentifiers(array($identifier));
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultConfiguration() {
$query = $this->newConfigurationQuery()
->withIdentifiers(
array(
self::EDITENGINECONFIG_DEFAULT,
))
->withIgnoreDatabaseConfigurations(true);
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultCreateConfiguration() {
$query = $this->newConfigurationQuery()
->withIsDefault(true)
->withIsDisabled(false);
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getCreateSortKey');
}
public function loadDefaultEditConfiguration($object) {
$query = $this->newConfigurationQuery()
->withIsEdit(true)
->withIsDisabled(false);
// If this object supports subtyping, we edit it with a form of the same
// subtype: so "bug" tasks get edited with "bug" forms.
if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
$query->withSubtypes(
array(
$object->getEditEngineSubtype(),
));
}
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getEditSortKey');
}
final public function getBuiltinEngineConfigurations() {
$configurations = $this->newBuiltinEngineConfigurations();
if (!$configurations) {
throw new Exception(
pht(
'EditEngine ("%s") returned no builtin engine configurations, but '.
'an edit engine must have at least one configuration.',
get_class($this)));
}
assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration');
$has_default = false;
foreach ($configurations as $config) {
if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
$has_default = true;
}
}
if (!$has_default) {
$first = head($configurations);
if (!$first->getBuiltinKey()) {
$first
->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
->setIsDefault(true)
->setIsEdit(true);
if (!strlen($first->getName())) {
$first->setName($this->getObjectCreateShortText());
}
} else {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but none are marked as default and the first configuration has '.
'a different builtin key already. Mark a builtin as default or '.
'omit the key from the first configuration',
get_class($this)));
}
}
$builtins = array();
foreach ($configurations as $key => $config) {
$builtin_key = $config->getBuiltinKey();
if ($builtin_key === null) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but one (with key "%s") is missing a builtin key. Provide a '.
'builtin key for each configuration (you can omit it from the '.
'first configuration in the list to automatically assign the '.
'default key).',
get_class($this),
$key));
}
if (isset($builtins[$builtin_key])) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but at least two specify the same builtin key ("%s"). Engines '.
'must have unique builtin keys.',
get_class($this),
$builtin_key));
}
$builtins[$builtin_key] = $config;
}
return $builtins;
}
protected function newBuiltinEngineConfigurations() {
return array(
$this->newConfiguration(),
);
}
final protected function newConfiguration() {
return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
$this->getViewer(),
$this);
}
/* -( Managing URIs )------------------------------------------------------ */
/**
* @task uri
*/
abstract protected function getObjectViewURI($object);
/**
* @task uri
*/
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI();
}
/**
* @task uri
*/
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
/**
* @task uri
*/
protected function getObjectEditCancelURI($object) {
return $this->getObjectViewURI($object);
}
/**
* @task uri
*/
public function getEditURI($object = null, $path = null) {
$parts = array();
$parts[] = $this->getEditorURI();
if ($object && $object->getID()) {
$parts[] = $object->getID().'/';
}
if ($path !== null) {
$parts[] = $path;
}
return implode('', $parts);
}
public function getEffectiveObjectViewURI($object) {
if ($this->getIsCreate()) {
return $this->getObjectViewURI($object);
}
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectViewURI($object);
}
public function getEffectiveObjectEditDoneURI($object) {
return $this->getEffectiveObjectViewURI($object);
}
public function getEffectiveObjectEditCancelURI($object) {
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectEditCancelURI($object);
}
/* -( Creating and Loading Objects )--------------------------------------- */
/**
* Initialize a new object for creation.
*
* @return object Newly initialized object.
* @task load
*/
abstract protected function newEditableObject();
/**
* Build an empty query for objects.
*
* @return PhabricatorPolicyAwareQuery Query.
* @task load
*/
abstract protected function newObjectQuery();
/**
* Test if this workflow is creating a new object or editing an existing one.
*
* @return bool True if a new object is being created.
* @task load
*/
final public function getIsCreate() {
return $this->isCreate;
}
/**
* Initialize a new object for object creation via Conduit.
*
* @return object Newly initialized object.
* @param list<wild> Raw transactions.
* @task load
*/
protected function newEditableObjectFromConduit(array $raw_xactions) {
return $this->newEditableObject();
}
/**
* Initialize a new object for documentation creation.
*
* @return object Newly initialized object.
* @task load
*/
protected function newEditableObjectForDocumentation() {
return $this->newEditableObject();
}
/**
* Flag this workflow as a create or edit.
*
* @param bool True if this is a create workflow.
* @return this
* @task load
*/
private function setIsCreate($is_create) {
$this->isCreate = $is_create;
return $this;
}
/**
* Try to load an object by ID, PHID, or monogram. This is done primarily
* to make Conduit a little easier to use.
*
* @param wild ID, PHID, or monogram.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object Corresponding editable object.
* @task load
*/
private function newObjectFromIdentifier(
$identifier,
array $capabilities = array()) {
if (is_int($identifier) || ctype_digit($identifier)) {
$object = $this->newObjectFromID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with ID "%s".',
$identifier));
}
return $object;
}
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
if (phid_get_type($identifier) != $type_unknown) {
$object = $this->newObjectFromPHID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with PHID "%s".',
$identifier));
}
return $object;
}
$target = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames(array($identifier))
->executeOne();
if (!$target) {
throw new Exception(
pht(
'Monogram "%s" does not identify a valid object.',
$identifier));
}
$expect = $this->newEditableObject();
$expect_class = get_class($expect);
$target_class = get_class($target);
if ($expect_class !== $target_class) {
throw new Exception(
pht(
'Monogram "%s" identifies an object of the wrong type. Loaded '.
'object has class "%s", but this editor operates on objects of '.
'type "%s".',
$identifier,
$target_class,
$expect_class));
}
// Load the object by PHID using this engine's standard query. This makes
// sure it's really valid, goes through standard policy check logic, and
// picks up any `need...()` clauses we want it to load with.
$object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
if (!$object) {
throw new Exception(
pht(
'Failed to reload object identified by monogram "%s" when '.
'querying by PHID.',
$identifier));
}
return $object;
}
/**
* Load an object by ID.
*
* @param int Object ID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromID($id, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withIDs(array($id));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object by PHID.
*
* @param phid Object PHID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromPHID($phid, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withPHIDs(array($phid));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object given a configured query.
*
* @param PhabricatorPolicyAwareQuery Configured query.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromQuery(
PhabricatorPolicyAwareQuery $query,
array $capabilities = array()) {
$viewer = $this->getViewer();
if (!$capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
$object = $query
->setViewer($viewer)
->requireCapabilities($capabilities)
->executeOne();
if (!$object) {
return null;
}
return $object;
}
/**
* Verify that an object is appropriate for editing.
*
* @param wild Loaded value.
* @return void
* @task load
*/
private function validateObject($object) {
if (!$object || !is_object($object)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object must '.
'actually be an object, but is of some other type ("%s").',
get_class($this),
gettype($object)));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object (of '.
'class "%s") must implement "%s", but does not.',
get_class($this),
get_class($object),
'PhabricatorApplicationTransactionInterface'));
}
}
/* -( Responding to Web Requests )----------------------------------------- */
final public function buildResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$action = $this->getEditAction();
$capabilities = array();
$use_default = false;
$require_create = true;
switch ($action) {
case 'comment':
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$use_default = true;
break;
case 'parameters':
$use_default = true;
break;
case 'nodefault':
case 'nocreate':
case 'nomanage':
$require_create = false;
break;
default:
break;
}
$object = $this->getTargetObject();
if (!$object) {
$id = $request->getURIData('id');
if ($id) {
$this->setIsCreate(false);
$object = $this->newObjectFromID($id, $capabilities);
if (!$object) {
return new Aphront404Response();
}
} else {
// Make sure the viewer has permission to create new objects of
// this type if we're going to create a new object.
if ($require_create) {
$this->requireCreateCapability();
}
$this->setIsCreate(true);
$object = $this->newEditableObject();
}
} else {
$id = $object->getID();
}
$this->validateObject($object);
if ($use_default) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return new Aphront404Response();
}
} else {
$form_key = $request->getURIData('formKey');
if (strlen($form_key)) {
$config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
if (!$config) {
return new Aphront404Response();
}
if ($id && !$config->getIsEdit()) {
return $this->buildNotEditFormRespose($object, $config);
}
} else {
if ($id) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return $this->buildNoEditResponse($object);
}
} else {
$config = $this->loadDefaultCreateConfiguration();
if (!$config) {
return $this->buildNoCreateResponse($object);
}
}
}
}
if ($config->getIsDisabled()) {
return $this->buildDisabledFormResponse($object, $config);
}
$page_key = $request->getURIData('pageKey');
if (!strlen($page_key)) {
$pages = $this->getPages($object);
if ($pages) {
$page_key = head_key($pages);
}
}
if (strlen($page_key)) {
$page = $this->selectPage($object, $page_key);
if (!$page) {
return new Aphront404Response();
}
}
switch ($action) {
case 'parameters':
return $this->buildParametersResponse($object);
case 'nodefault':
return $this->buildNoDefaultResponse($object);
case 'nocreate':
return $this->buildNoCreateResponse($object);
case 'nomanage':
return $this->buildNoManageResponse($object);
case 'comment':
return $this->buildCommentResponse($object);
default:
return $this->buildEditResponse($object);
}
}
private function buildCrumbs($object, $final = false) {
$controller = $this->getController();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if ($this->getIsCreate()) {
$create_text = $this->getObjectCreateShortText();
if ($final) {
$crumbs->addTextCrumb($create_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($create_text, $edit_uri);
}
} else {
$crumbs->addTextCrumb(
$this->getObjectEditShortText($object),
$this->getEffectiveObjectViewURI($object));
$edit_text = pht('Edit');
if ($final) {
$crumbs->addTextCrumb($edit_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($edit_text, $edit_uri);
}
}
return $crumbs;
}
private function buildEditResponse($object) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$template = $object->getApplicationTransactionTemplate();
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
$config = $this->getEditEngineConfiguration()
->attachEngine($this);
// NOTE: Don't prompt users to override locks when creating objects,
// even if the default settings would create a locked object.
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact &&
!$this->getIsCreate() &&
!$request->getBool('editEngine') &&
!$request->getBool('overrideLock')) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->getController()
->newDialog()
->addHiddenInput('overrideLock', true)
->setDisableWorkflowOnSubmit(true)
->addCancelButton($cancel_uri);
return $lock->willPromptUserForLockOverrideWithDialog($dialog);
}
$validation_exception = null;
if ($request->isFormPost() && $request->getBool('editEngine')) {
$submit_fields = $fields;
foreach ($submit_fields as $key => $field) {
if (!$field->shouldGenerateTransactionsFromSubmit()) {
unset($submit_fields[$key]);
continue;
}
}
// Before we read the submitted values, store a copy of what we would
// use if the form was empty so we can figure out which transactions are
// just setting things to their default values for the current form.
$defaults = array();
foreach ($submit_fields as $key => $field) {
$defaults[$key] = $field->getValueForTransaction();
}
foreach ($submit_fields as $key => $field) {
$field->setIsSubmittedForm(true);
if (!$field->shouldReadValueFromSubmit()) {
continue;
}
$field->readValueFromSubmit($request);
}
$xactions = array();
if ($this->getIsCreate()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
if ($this->supportsSubtypes()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
->setNewValue($config->getSubtype());
}
}
foreach ($submit_fields as $key => $field) {
$field_value = $field->getValueForTransaction();
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field_value,
));
foreach ($type_xactions as $type_xaction) {
$default = $defaults[$key];
if ($default === $field->getValueForTransaction()) {
$type_xaction->setIsDefaultTransaction(true);
}
$xactions[] = $type_xaction;
}
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$xactions = $this->willApplyTransactions($object, $xactions);
$editor->applyTransactions($object, $xactions);
$this->didApplyTransactions($object, $xactions);
return $this->newEditResponse($request, $object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
foreach ($fields as $field) {
$message = $this->getValidationExceptionShortMessage($ex, $field);
if ($message === null) {
continue;
}
$field->setControlError($message);
}
}
} else {
if ($this->getIsCreate()) {
$template = $request->getStr('template');
if (strlen($template)) {
$template_object = $this->newObjectFromIdentifier(
$template,
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
if (!$template_object) {
return new Aphront404Response();
}
} else {
$template_object = null;
}
if ($template_object) {
$copy_fields = $this->buildEditFields($template_object);
$copy_fields = mpull($copy_fields, null, 'getKey');
foreach ($copy_fields as $copy_key => $copy_field) {
if (!$copy_field->getIsCopyable()) {
unset($copy_fields[$copy_key]);
}
}
} else {
$copy_fields = array();
}
foreach ($fields as $field) {
if (!$field->shouldReadValueFromRequest()) {
continue;
}
$field_key = $field->getKey();
if (isset($copy_fields[$field_key])) {
$field->readValueFromField($copy_fields[$field_key]);
}
$field->readValueFromRequest($request);
}
}
}
$action_button = $this->buildEditFormActionButton($object);
if ($this->getIsCreate()) {
$header_text = $this->getFormHeaderText($object);
} else {
$header_text = $this->getObjectEditTitleText($object);
}
$show_preview = !$request->isAjax();
if ($show_preview) {
$previews = array();
foreach ($fields as $field) {
$preview = $field->getPreviewPanel();
if (!$preview) {
continue;
}
$control_id = $field->getControlID();
$preview
->setControlID($control_id)
->setPreviewURI('/transactions/remarkuppreview/');
$previews[] = $preview;
}
} else {
$previews = array();
}
$form = $this->buildEditForm($object, $fields);
$crumbs = $this->buildCrumbs($object, $final = true);
$crumbs->setBorder(true);
if ($request->isAjax()) {
return $this->getController()
->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_text)
->setValidationException($validation_exception)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
$box_header = id(new PHUIHeaderView())
->setHeader($header_text);
if ($action_button) {
$box_header->addActionLink($action_button);
}
$box = id(new PHUIObjectBoxView())
->setUser($viewer)
->setHeader($box_header)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($form);
// This is fairly questionable, but in use by Settings.
if ($request->getURIData('formSaved')) {
$box->setFormSaved(true);
}
$content = array(
$box,
$previews,
);
$view = new PHUITwoColumnView();
$page_header = $this->getPageHeader($object);
if ($page_header) {
$view->setHeader($page_header);
}
$page = $controller->newPage()
->setTitle($header_text)
->setCrumbs($crumbs)
->appendChild($view);
$navigation = $this->getNavigation();
if ($navigation) {
$view->setFixed(true);
$view->setNavigation($navigation);
$view->setMainColumn($content);
} else {
$view->setFooter($content);
}
return $page;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
return id(new AphrontRedirectResponse())
->setURI($this->getEffectiveObjectEditDoneURI($object));
}
private function buildEditForm($object, array $fields) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->willBuildEditForm($object, $fields);
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param));
}
foreach ($fields as $field) {
$field->appendToForm($form);
}
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
if (!$request->isAjax()) {
$buttons = id(new AphrontFormSubmitControl())
->setValue($submit_button);
if ($cancel_uri) {
$buttons->addCancelButton($cancel_uri);
}
$form->appendControl($buttons);
}
return $form;
}
protected function willBuildEditForm($object, array $fields) {
return $fields;
}
private function buildEditFormActionButton($object) {
if (!$this->isEngineConfigurable()) {
return null;
}
$viewer = $this->getViewer();
$action_view = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($this->buildEditFormActions($object) as $action) {
$action_view->addAction($action);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Configure Form'))
->setHref('#')
->setIcon('fa-gear')
->setDropdownMenu($action_view);
return $action_button;
}
private function buildEditFormActions($object) {
$actions = array();
// c4science custo (moved)
$config = $this->getEditEngineConfiguration();
$can_manage = PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$config,
PhabricatorPolicyCapability::CAN_EDIT);
// end of c4s custo
if ($this->supportsEditEngineConfiguration()) {
$engine_key = $this->getEngineKey();
if ($can_manage) {
$manage_uri = $config->getURI();
} else {
$manage_uri = $this->getEditURI(null, 'nomanage/');
}
$view_uri = "/transactions/editengine/{$engine_key}/";
if($can_manage) { // c4science custo
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Configuration'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('View Form Configurations'))
->setIcon('fa-list-ul')
->setHref($view_uri);
$actions[] = id(new PhabricatorActionView())
->setName(pht('Edit Form Configuration'))
->setIcon('fa-pencil')
->setHref($manage_uri)
->setDisabled(!$can_manage)
->setWorkflow(!$can_manage);
} // end of c4s custo
}
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('Using HTTP Parameters'))
->setIcon('fa-book')
->setHref($this->getEditURI($object, 'parameters/'));
if($can_manage) { // c4science custo
$doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
$actions[] = id(new PhabricatorActionView())
->setName(pht('User Guide: Customizing Forms'))
->setIcon('fa-book')
->setHref($doc_href);
} // end of c4s custo
return $actions;
}
public function newNUXButton($text) {
$specs = $this->newCreateActionSpecifications(array());
$head = head($specs);
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($head['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow'])
->setColor(PHUIButtonView::GREEN);
}
final public function addActionToCrumbs(
PHUICrumbsView $crumbs,
array $parameters = array()) {
$viewer = $this->getViewer();
$specs = $this->newCreateActionSpecifications($parameters);
$head = head($specs);
$menu_uri = $head['uri'];
$dropdown = null;
if (count($specs) > 1) {
$menu_icon = 'fa-caret-square-o-down';
$menu_name = $this->getObjectCreateShortText();
$workflow = false;
$disabled = false;
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($specs as $spec) {
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($spec['name'])
->setIcon($spec['icon'])
->setHref($spec['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow']));
}
} else {
$menu_icon = $head['icon'];
$menu_name = $head['name'];
$workflow = $head['workflow'];
$disabled = $head['disabled'];
}
$action = id(new PHUIListItemView())
->setName($menu_name)
->setHref($menu_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
}
/**
* Build a raw description of available "Create New Object" UI options so
* other methods can build menus or buttons.
*/
public function newCreateActionSpecifications(array $parameters) {
$viewer = $this->getViewer();
$can_create = $this->hasCreateCapability();
if ($can_create) {
$configs = $this->loadUsableConfigurationsForCreate();
} else {
$configs = array();
}
$disabled = false;
$workflow = false;
$menu_icon = 'fa-plus-square';
$specs = array();
if (!$configs) {
if ($viewer->isLoggedIn()) {
$disabled = true;
} else {
// If the viewer isn't logged in, assume they'll get hit with a login
// dialog and are likely able to create objects after they log in.
$disabled = false;
}
$workflow = true;
if ($can_create) {
$create_uri = $this->getEditURI(null, 'nodefault/');
} else {
$create_uri = $this->getEditURI(null, 'nocreate/');
}
$specs[] = array(
'name' => $this->getObjectCreateShortText(),
'uri' => $create_uri,
'icon' => $menu_icon,
'disabled' => $disabled,
'workflow' => $workflow,
);
} else {
foreach ($configs as $config) {
$config_uri = $config->getCreateURI();
if ($parameters) {
$config_uri = (string)id(new PhutilURI($config_uri))
->setQueryParams($parameters);
}
$specs[] = array(
'name' => $config->getDisplayName(),
'uri' => $config_uri,
'icon' => 'fa-plus',
'disabled' => false,
'workflow' => false,
);
}
}
return $specs;
}
final public function buildEditEngineCommentView($object) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
// TODO: This just nukes the entire comment form if you don't have access
// to any edit forms. We might want to tailor this UX a bit.
return id(new PhabricatorApplicationTransactionCommentView())
->setNoPermission(true);
}
$viewer = $this->getViewer();
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return id(new PhabricatorApplicationTransactionCommentView())
->setEditEngineLock($lock);
}
$object_phid = $object->getPHID();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$header_text = $this->getCommentViewSeriousHeaderText($object);
$button_text = $this->getCommentViewSeriousButtonText($object);
} else {
$header_text = $this->getCommentViewHeaderText($object);
$button_text = $this->getCommentViewButtonText($object);
}
$comment_uri = $this->getEditURI($object, 'comment/');
$view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($object_phid)
->setHeaderText($header_text)
->setAction($comment_uri)
->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft(
$object_phid,
$viewer->getPHID());
if ($draft) {
$view->setVersionedDraft($draft);
}
$view->setCurrentVersion($this->loadDraftVersion($object));
$fields = $this->buildEditFields($object);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$comment_actions = array();
foreach ($fields as $field) {
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
continue;
}
}
$comment_action = $field->getCommentAction();
if (!$comment_action) {
continue;
}
$key = $comment_action->getKey();
// TODO: Validate these better.
$comment_actions[$key] = $comment_action;
}
$comment_actions = msortv($comment_actions, 'getSortVector');
$view->setCommentActions($comment_actions);
$comment_groups = $this->newCommentActionGroups();
$view->setCommentActionGroups($comment_groups);
return $view;
}
protected function loadDraftVersion($object) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
return null;
}
$template = $object->getApplicationTransactionTemplate();
$conn_r = $template->establishConnection('r');
// Find the most recent transaction the user has written. We'll use this
// as a version number to make sure that out-of-date drafts get discarded.
$result = queryfx_one(
$conn_r,
'SELECT id AS version FROM %T
WHERE objectPHID = %s AND authorPHID = %s
ORDER BY id DESC LIMIT 1',
$template->getTableName(),
$object->getPHID(),
$viewer->getPHID());
if ($result) {
return (int)$result['version'];
} else {
return null;
}
}
/* -( Responding to HTTP Parameter Requests )------------------------------ */
/**
* Respond to a request for documentation on HTTP parameters.
*
* @param object Editable object.
* @return AphrontResponse Response object.
* @task http
*/
private function buildParametersResponse($object) {
$controller = $this->getController();
$viewer = $this->getViewer();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$crumbs = $this->buildCrumbs($object);
$crumbs->addTextCrumb(pht('HTTP Parameters'));
$crumbs->setBorder(true);
$header_text = pht(
'HTTP Parameters: %s',
$this->getObjectCreateShortText());
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
->setUser($viewer)
->setFields($fields);
- $document = id(new PHUIDocumentViewPro())
+ $document = id(new PHUIDocumentView())
->setUser($viewer)
->setHeader($header)
->appendChild($help_view);
return $controller->newPage()
->setTitle(pht('HTTP Parameters'))
->setCrumbs($crumbs)
->appendChild($document);
}
private function buildError($object, $title, $body) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$dialog = $this->getController()
->newDialog()
->addCancelButton($cancel_uri);
if ($title !== null) {
$dialog->setTitle($title);
}
if ($body !== null) {
$dialog->appendParagraph($body);
}
return $dialog;
}
private function buildNoDefaultResponse($object) {
return $this->buildError(
$object,
pht('No Default Create Forms'),
pht(
'This application is not configured with any forms for creating '.
'objects that are visible to you and enabled.'));
}
private function buildNoCreateResponse($object) {
return $this->buildError(
$object,
pht('No Create Permission'),
pht('You do not have permission to create these objects.'));
}
private function buildNoManageResponse($object) {
return $this->buildError(
$object,
pht('No Manage Permission'),
pht(
'You do not have permission to configure forms for this '.
'application.'));
}
private function buildNoEditResponse($object) {
return $this->buildError(
$object,
pht('No Edit Forms'),
pht(
'You do not have access to any forms which are enabled and marked '.
'as edit forms.'));
}
private function buildNotEditFormRespose($object, $config) {
return $this->buildError(
$object,
pht('Not an Edit Form'),
pht(
'This form ("%s") is not marked as an edit form, so '.
'it can not be used to edit objects.',
$config->getName()));
}
private function buildDisabledFormResponse($object, $config) {
return $this->buildError(
$object,
pht('Form Disabled'),
pht(
'This form ("%s") has been disabled, so it can not be used.',
$config->getName()));
}
private function buildLockedObjectResponse($object) {
$dialog = $this->buildError($object, null, null);
$viewer = $this->getViewer();
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return $lock->willBlockUserInteractionWithDialog($dialog);
}
private function buildCommentResponse($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
return new Aphront404Response();
}
$controller = $this->getController();
$request = $controller->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
return $this->buildLockedObjectResponse($object);
}
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return new Aphront404Response();
}
$fields = $this->buildEditFields($object);
$is_preview = $request->isPreviewRequest();
$view_uri = $this->getEffectiveObjectViewURI($object);
$template = $object->getApplicationTransactionTemplate();
$comment_template = $template->getApplicationTransactionCommentObject();
$comment_text = $request->getStr('comment');
$actions = $request->getStr('editengine.actions');
if ($actions) {
$actions = phutil_json_decode($actions);
}
if ($is_preview) {
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$request_version = $request->getInt($version_key);
$current_version = $this->loadDraftVersion($object);
if ($request_version >= $current_version) {
$draft = PhabricatorVersionedDraft::loadOrCreateDraft(
$object->getPHID(),
$viewer->getPHID(),
$current_version);
$is_empty = (!strlen($comment_text) && !$actions);
$draft
->setProperty('comment', $comment_text)
->setProperty('actions', $actions)
->save();
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft($draft)
->synchronize();
}
}
}
$xactions = array();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($actions) {
$action_map = array();
foreach ($actions as $action) {
$type = idx($action, 'type');
if (!$type) {
continue;
}
if (empty($fields[$type])) {
continue;
}
$action_map[$type] = $action;
}
foreach ($action_map as $type => $action) {
$field = $fields[$type];
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
// If you don't have edit permission on the object, you're limited in
// which actions you can take via the comment form. Most actions
// need edit permission, but some actions (like "Accept Revision")
// can be applied by anyone with view permission.
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
// We know the user doesn't have the capability, so this will
// raise a policy exception.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
if (array_key_exists('initialValue', $action)) {
$field->setInitialValue($action['initialValue']);
}
$field->readValueFromComment(idx($action, 'value'));
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field->getValueForTransaction(),
));
foreach ($type_xactions as $type_xaction) {
$xactions[] = $type_xaction;
}
}
}
$auto_xactions = $this->newAutomaticCommentTransactions($object);
foreach ($auto_xactions as $xaction) {
$xactions[] = $xaction;
}
if (strlen($comment_text) || !$xactions) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
return id(new PhabricatorApplicationTransactionValidationResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionWarningException $ex) {
return id(new PhabricatorApplicationTransactionWarningResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if (!$is_preview) {
PhabricatorVersionedDraft::purgeDrafts(
$object->getPHID(),
$viewer->getPHID());
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft(null)
->synchronize();
}
}
if ($request->isAjax() && $is_preview) {
$preview_content = $this->newCommentPreviewContent($object, $xactions);
return id(new PhabricatorApplicationTransactionResponse())
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview)
->setPreviewContent($preview_content);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
protected function newDraftEngine($object) {
$viewer = $this->getViewer();
if ($object instanceof PhabricatorDraftInterface) {
$engine = $object->newDraftEngine();
} else {
$engine = new PhabricatorBuiltinDraftEngine();
}
return $engine
->setObject($object)
->setViewer($viewer);
}
/* -( Conduit )------------------------------------------------------------ */
/**
* Respond to a Conduit edit request.
*
* This method accepts a list of transactions to apply to an object, and
* either edits an existing object or creates a new one.
*
* @task conduit
*/
final public function buildConduitResponse(ConduitAPIRequest $request) {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht(
'Unable to load configuration for this EditEngine ("%s").',
get_class($this)));
}
$raw_xactions = $this->getRawConduitTransactions($request);
$identifier = $request->getValue('objectIdentifier');
if ($identifier) {
$this->setIsCreate(false);
- $object = $this->newObjectFromIdentifier($identifier);
+
+ // After T13186, each transaction can individually weaken or replace the
+ // capabilities required to apply it, so we no longer need CAN_EDIT to
+ // attempt to apply transactions to objects. In practice, almost all
+ // transactions require CAN_EDIT so we won't get very far if we don't
+ // have it.
+ $capabilities = array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ );
+
+ $object = $this->newObjectFromIdentifier(
+ $identifier,
+ $capabilities);
} else {
$this->requireCreateCapability();
$this->setIsCreate(true);
$object = $this->newEditableObjectFromConduit($raw_xactions);
}
$this->validateObject($object);
$fields = $this->buildEditFields($object);
$types = $this->getConduitEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$xactions = $this->getConduitTransactions(
$request,
$raw_xactions,
$types,
$template);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true);
if (!$this->getIsCreate()) {
$editor->setContinueOnMissingFields(true);
}
$xactions = $editor->applyTransactions($object, $xactions);
$xactions_struct = array();
foreach ($xactions as $xaction) {
$xactions_struct[] = array(
'phid' => $xaction->getPHID(),
);
}
return array(
'object' => array(
'id' => (int)$object->getID(),
'phid' => $object->getPHID(),
),
'transactions' => $xactions_struct,
);
}
private function getRawConduitTransactions(ConduitAPIRequest $request) {
$transactions_key = 'transactions';
$xactions = $request->getValue($transactions_key);
if (!is_array($xactions)) {
throw new Exception(
pht(
'Parameter "%s" is not a list of transactions.',
$transactions_key));
}
foreach ($xactions as $key => $xaction) {
if (!is_array($xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is not a dictionary.',
$transactions_key,
$key));
}
if (!array_key_exists('type', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "type" field. Each '.
'transaction must have a type field.',
$transactions_key,
$key));
}
}
return $xactions;
}
/**
* Generate transactions which can be applied from edit actions in a Conduit
* request.
*
* @param ConduitAPIRequest The request.
* @param list<wild> Raw conduit transactions.
* @param list<PhabricatorEditType> Supported edit types.
* @param PhabricatorApplicationTransaction Template transaction.
* @return list<PhabricatorApplicationTransaction> Generated transactions.
* @task conduit
*/
private function getConduitTransactions(
ConduitAPIRequest $request,
array $xactions,
array $types,
PhabricatorApplicationTransaction $template) {
$viewer = $request->getUser();
$results = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction['type'];
if (empty($types[$type])) {
throw new Exception(
pht(
'Transaction with key "%s" has invalid type "%s". This type is '.
'not recognized. Valid types are: %s.',
$key,
$type,
implode(', ', array_keys($types))));
}
}
if ($this->getIsCreate()) {
$results[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
$is_strict = $request->getIsStrictlyTyped();
foreach ($xactions as $xaction) {
$type = $types[$xaction['type']];
// Let the parameter type interpret the value. This allows you to
// use usernames in list<user> fields, for example.
$parameter_type = $type->getConduitParameterType();
$parameter_type->setViewer($viewer);
try {
$value = $xaction['value'];
$value = $parameter_type->getValue($xaction, 'value', $is_strict);
$value = $type->getTransactionValueFromConduit($value);
$xaction['value'] = $value;
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception when processing transaction of type "%s": %s',
$xaction['type'],
$ex->getMessage()),
$ex);
}
$type_xactions = $type->generateTransactions(
clone $template,
$xaction);
foreach ($type_xactions as $type_xaction) {
$results[] = $type_xaction;
}
}
return $results;
}
/**
* @return map<string, PhabricatorEditType>
* @task conduit
*/
private function getConduitEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getConduitEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
public function getConduitEditTypes() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return array();
}
$object = $this->newEditableObjectForDocumentation();
$fields = $this->buildEditFields($object);
return $this->getConduitEditTypesFromFields($fields);
}
final public static function getAllEditEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getEngineKey')
->execute();
}
final public static function getByKey(PhabricatorUser $viewer, $key) {
return id(new PhabricatorEditEngineQuery())
->setViewer($viewer)
->withEngineKeys(array($key))
->executeOne();
}
public function getIcon() {
$application = $this->getApplication();
return $application->getIcon();
}
private function loadUsableConfigurationsForCreate() {
$viewer = $this->getViewer();
$configs = id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($viewer)
->withEngineKeys(array($this->getEngineKey()))
->withIsDefault(true)
->withIsDisabled(false)
->execute();
$configs = msort($configs, 'getCreateSortKey');
// Attach this specific engine to configurations we load so they can access
// any runtime configuration. For example, this allows us to generate the
// correct "Create Form" buttons when editing forms, see T12301.
foreach ($configs as $config) {
$config->attachEngine($this);
}
return $configs;
}
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
$xaction_type = $field->getTransactionType();
if ($xaction_type === null) {
return null;
}
return $ex->getShortMessage($xaction_type);
}
protected function getCreateNewObjectPolicy() {
return PhabricatorPolicies::POLICY_USER;
}
private function requireCreateCapability() {
PhabricatorPolicyFilter::requireCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
private function hasCreateCapability() {
return PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
public function isCommentAction() {
return ($this->getEditAction() == 'comment');
}
public function getEditAction() {
$controller = $this->getController();
$request = $controller->getRequest();
return $request->getURIData('editAction');
}
protected function newCommentActionGroups() {
return array();
}
protected function newAutomaticCommentTransactions($object) {
return array();
}
protected function newCommentPreviewContent($object, array $xactions) {
return null;
}
/* -( Form Pages )--------------------------------------------------------- */
public function getSelectedPage() {
return $this->page;
}
private function selectPage($object, $page_key) {
$pages = $this->getPages($object);
if (empty($pages[$page_key])) {
return null;
}
$this->page = $pages[$page_key];
return $this->page;
}
protected function newPages($object) {
return array();
}
protected function getPages($object) {
if ($this->pages === null) {
$pages = $this->newPages($object);
assert_instances_of($pages, 'PhabricatorEditPage');
$pages = mpull($pages, null, 'getKey');
$this->pages = $pages;
}
return $this->pages;
}
private function applyPageToFields($object, array $fields) {
$pages = $this->getPages($object);
if (!$pages) {
return $fields;
}
if (!$this->getSelectedPage()) {
return $fields;
}
$page_picks = array();
$default_key = head($pages)->getKey();
foreach ($pages as $page_key => $page) {
foreach ($page->getFieldKeys() as $field_key) {
$page_picks[$field_key] = $page_key;
}
if ($page->getIsDefault()) {
$default_key = $page_key;
}
}
$page_map = array_fill_keys(array_keys($pages), array());
foreach ($fields as $field_key => $field) {
if (isset($page_picks[$field_key])) {
$page_map[$page_picks[$field_key]][$field_key] = $field;
continue;
}
// TODO: Maybe let the field pick a page to associate itself with so
// extensions can force themselves onto a particular page?
$page_map[$default_key][$field_key] = $field;
}
$page = $this->getSelectedPage();
if (!$page) {
$page = head($pages);
}
$selected_key = $page->getKey();
return $page_map[$selected_key];
}
protected function willApplyTransactions($object, array $xactions) {
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
return;
}
/* -( Bulk Edits )--------------------------------------------------------- */
final public function newBulkEditGroupMap() {
$groups = $this->newBulkEditGroups();
$map = array();
foreach ($groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
'group must have a unique key.',
$key));
}
$map[$key] = $group;
}
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension_groups = $extension->newBulkEditGroups($this);
foreach ($extension_groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Extension "%s" defines a bulk edit group with the same key '.
'("%s") as the main editor or another extension. Each bulk '.
'edit group must have a unique key.'));
}
$map[$key] = $group;
}
}
return $map;
}
protected function newBulkEditGroups() {
return array(
id(new PhabricatorBulkEditGroup())
->setKey('default')
->setLabel(pht('Primary Fields')),
id(new PhabricatorBulkEditGroup())
->setKey('extension')
->setLabel(pht('Support Applications')),
);
}
final public function newBulkEditMap() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$groups = $this->newBulkEditGroupMap();
$edit_types = $this->getBulkEditTypesFromFields($fields);
$map = array();
foreach ($edit_types as $key => $type) {
$bulk_type = $type->getBulkParameterType();
if ($bulk_type === null) {
continue;
}
$bulk_label = $type->getBulkEditLabel();
if ($bulk_label === null) {
continue;
}
$group_key = $type->getBulkEditGroupKey();
if (!$group_key) {
$group_key = 'default';
}
if (!isset($groups[$group_key])) {
throw new Exception(
pht(
'Field "%s" has a bulk edit group key ("%s") with no '.
'corresponding bulk edit group.',
$key,
$group_key));
}
$map[] = array(
'label' => $bulk_label,
'xaction' => $key,
'group' => $group_key,
'control' => array(
'type' => $bulk_type->getPHUIXControlType(),
'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
),
);
}
return $map;
}
final public function newRawBulkTransactions(array $xactions) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$edit_types = $this->getBulkEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$raw_xactions = array();
foreach ($xactions as $key => $xaction) {
PhutilTypeSpec::checkMap(
$xaction,
array(
'type' => 'string',
'value' => 'optional wild',
));
$type = $xaction['type'];
if (!isset($edit_types[$type])) {
throw new Exception(
pht(
'Unsupported bulk edit type "%s".',
$type));
}
$edit_type = $edit_types[$type];
// Replace the edit type with the underlying transaction type. Usually
// these are 1:1 and the transaction type just has more internal noise,
// but it's possible that this isn't the case.
$xaction['type'] = $edit_type->getTransactionType();
$value = $xaction['value'];
$value = $edit_type->getTransactionValueFromBulkEdit($value);
$xaction['value'] = $value;
$xaction_objects = $edit_type->generateTransactions(
clone $template,
$xaction);
foreach ($xaction_objects as $xaction_object) {
$raw_xaction = array(
'type' => $xaction_object->getTransactionType(),
'metadata' => $xaction_object->getMetadata(),
'new' => $xaction_object->getNewValue(),
);
if ($xaction_object->hasOldValue()) {
$raw_xaction['old'] = $xaction_object->getOldValue();
}
if ($xaction_object->hasComment()) {
$comment = $xaction_object->getComment();
$raw_xaction['comment'] = $comment->getContent();
}
$raw_xactions[] = $raw_xaction;
}
}
return $raw_xactions;
}
private function getBulkEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getBulkEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return get_class($this);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getCreateNewObjectPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/transactions/editfield/PhabricatorCheckboxesEditField.php b/src/applications/transactions/editfield/PhabricatorCheckboxesEditField.php
new file mode 100644
index 000000000..82d85c442
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorCheckboxesEditField.php
@@ -0,0 +1,36 @@
+<?php
+
+final class PhabricatorCheckboxesEditField
+ extends PhabricatorEditField {
+
+ private $options;
+
+ protected function newControl() {
+ $options = $this->getOptions();
+
+ return id(new AphrontFormCheckboxControl())
+ ->setOptions($options);
+ }
+
+ protected function newConduitParameterType() {
+ return new ConduitStringListParameterType();
+ }
+
+ protected function newHTTPParameterType() {
+ return new AphrontStringListHTTPParameterType();
+ }
+
+ public function setOptions(array $options) {
+ $this->options = $options;
+ return $this;
+ }
+
+ public function getOptions() {
+ if ($this->options === null) {
+ throw new PhutilInvalidStateException('setOptions');
+ }
+
+ return $this->options;
+ }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php
index 94c95a505..07bf3589a 100644
--- a/src/applications/transactions/editfield/PhabricatorEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorEditField.php
@@ -1,912 +1,920 @@
<?php
abstract class PhabricatorEditField extends Phobject {
private $key;
private $viewer;
private $label;
private $aliases = array();
private $value;
private $initialValue;
private $hasValue = false;
private $object;
private $transactionType;
private $metadata = array();
private $editTypeKey;
private $isRequired;
private $previewPanel;
private $controlID;
private $controlInstructions;
private $bulkEditLabel;
private $bulkEditGroupKey;
private $description;
private $conduitDescription;
private $conduitDocumentation;
private $conduitTypeDescription;
private $commentActionLabel;
private $commentActionValue;
private $commentActionGroupKey;
private $commentActionOrder = 1000;
private $hasCommentActionValue;
private $isLocked;
private $isHidden;
private $isPreview;
private $isEditDefaults;
private $isSubmittedForm;
private $controlError;
private $canApplyWithoutEditCapability = false;
private $isReorderable = true;
private $isDefaultable = true;
private $isLockable = true;
private $isCopyable = false;
private $isConduitOnly = false;
private $conduitEditTypes;
private $bulkEditTypes;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setBulkEditLabel($bulk_edit_label) {
$this->bulkEditLabel = $bulk_edit_label;
return $this;
}
public function getBulkEditLabel() {
return $this->bulkEditLabel;
}
public function setBulkEditGroupKey($key) {
$this->bulkEditGroupKey = $key;
return $this;
}
public function getBulkEditGroupKey() {
return $this->bulkEditGroupKey;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setAliases(array $aliases) {
$this->aliases = $aliases;
return $this;
}
public function getAliases() {
return $this->aliases;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setIsLocked($is_locked) {
$this->isLocked = $is_locked;
return $this;
}
public function getIsLocked() {
return $this->isLocked;
}
public function setIsPreview($preview) {
$this->isPreview = $preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsReorderable($is_reorderable) {
$this->isReorderable = $is_reorderable;
return $this;
}
public function getIsReorderable() {
return $this->isReorderable;
}
public function setIsConduitOnly($is_conduit_only) {
$this->isConduitOnly = $is_conduit_only;
return $this;
}
public function getIsConduitOnly() {
return $this->isConduitOnly;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setConduitDescription($conduit_description) {
$this->conduitDescription = $conduit_description;
return $this;
}
public function getConduitDescription() {
if ($this->conduitDescription === null) {
return $this->getDescription();
}
return $this->conduitDescription;
}
public function setConduitDocumentation($conduit_documentation) {
$this->conduitDocumentation = $conduit_documentation;
return $this;
}
public function getConduitDocumentation() {
return $this->conduitDocumentation;
}
public function setConduitTypeDescription($conduit_type_description) {
$this->conduitTypeDescription = $conduit_type_description;
return $this;
}
public function getConduitTypeDescription() {
return $this->conduitTypeDescription;
}
public function setIsEditDefaults($is_edit_defaults) {
$this->isEditDefaults = $is_edit_defaults;
return $this;
}
public function getIsEditDefaults() {
return $this->isEditDefaults;
}
public function setIsDefaultable($is_defaultable) {
$this->isDefaultable = $is_defaultable;
return $this;
}
public function getIsDefaultable() {
return $this->isDefaultable;
}
public function setIsLockable($is_lockable) {
$this->isLockable = $is_lockable;
return $this;
}
public function getIsLockable() {
return $this->isLockable;
}
public function setIsHidden($is_hidden) {
$this->isHidden = $is_hidden;
return $this;
}
public function getIsHidden() {
return $this->isHidden;
}
public function setIsCopyable($is_copyable) {
$this->isCopyable = $is_copyable;
return $this;
}
public function getIsCopyable() {
return $this->isCopyable;
}
public function setIsSubmittedForm($is_submitted) {
$this->isSubmittedForm = $is_submitted;
return $this;
}
public function getIsSubmittedForm() {
return $this->isSubmittedForm;
}
public function setIsRequired($is_required) {
$this->isRequired = $is_required;
return $this;
}
public function getIsRequired() {
return $this->isRequired;
}
public function setControlError($control_error) {
$this->controlError = $control_error;
return $this;
}
public function getControlError() {
return $this->controlError;
}
public function setCommentActionLabel($label) {
$this->commentActionLabel = $label;
return $this;
}
public function getCommentActionLabel() {
return $this->commentActionLabel;
}
public function setCommentActionGroupKey($key) {
$this->commentActionGroupKey = $key;
return $this;
}
public function getCommentActionGroupKey() {
return $this->commentActionGroupKey;
}
public function setCommentActionOrder($order) {
$this->commentActionOrder = $order;
return $this;
}
public function getCommentActionOrder() {
return $this->commentActionOrder;
}
public function setCommentActionValue($comment_action_value) {
$this->hasCommentActionValue = true;
$this->commentActionValue = $comment_action_value;
return $this;
}
public function getCommentActionValue() {
return $this->commentActionValue;
}
public function setPreviewPanel(PHUIRemarkupPreviewPanel $preview_panel) {
$this->previewPanel = $preview_panel;
return $this;
}
public function getPreviewPanel() {
+ if ($this->getIsHidden()) {
+ return null;
+ }
+
+ if ($this->getIsLocked()) {
+ return null;
+ }
+
return $this->previewPanel;
}
public function setControlInstructions($control_instructions) {
$this->controlInstructions = $control_instructions;
return $this;
}
public function getControlInstructions() {
return $this->controlInstructions;
}
public function setCanApplyWithoutEditCapability($can_apply) {
$this->canApplyWithoutEditCapability = $can_apply;
return $this;
}
public function getCanApplyWithoutEditCapability() {
return $this->canApplyWithoutEditCapability;
}
protected function newControl() {
throw new PhutilMethodNotImplementedException();
}
protected function buildControl() {
if ($this->getIsConduitOnly()) {
return null;
}
$control = $this->newControl();
if ($control === null) {
return null;
}
$control
->setValue($this->getValueForControl())
->setName($this->getKey());
if (!$control->getLabel()) {
$control->setLabel($this->getLabel());
}
if ($this->getIsSubmittedForm()) {
$error = $this->getControlError();
if ($error !== null) {
$control->setError($error);
}
} else if ($this->getIsRequired()) {
$control->setError(true);
}
return $control;
}
public function getControlID() {
if (!$this->controlID) {
$this->controlID = celerity_generate_unique_node_id();
}
return $this->controlID;
}
protected function renderControl() {
$control = $this->buildControl();
if ($control === null) {
return null;
}
if ($this->getIsPreview()) {
$disabled = true;
$hidden = false;
} else if ($this->getIsEditDefaults()) {
$disabled = false;
$hidden = false;
} else {
$disabled = $this->getIsLocked();
$hidden = $this->getIsHidden();
}
if ($hidden) {
return null;
}
$control->setDisabled($disabled);
if ($this->controlID) {
$control->setID($this->controlID);
}
return $control;
}
public function appendToForm(AphrontFormView $form) {
$control = $this->renderControl();
if ($control !== null) {
if ($this->getIsPreview()) {
if ($this->getIsHidden()) {
$control
->addClass('aphront-form-preview-hidden')
->setError(pht('Hidden'));
} else if ($this->getIsLocked()) {
$control
->setError(pht('Locked'));
}
}
$instructions = $this->getControlInstructions();
if (strlen($instructions)) {
$form->appendRemarkupInstructions($instructions);
}
$form->appendControl($control);
}
return $this;
}
protected function getValueForControl() {
return $this->getValue();
}
public function getValueForDefaults() {
$value = $this->getValue();
// By default, just treat the empty string like `null` since they're
// equivalent for almost all fields and this reduces the number of
// meaningless transactions we generate when adjusting defaults.
if ($value === '') {
return null;
}
return $value;
}
protected function getValue() {
return $this->value;
}
public function setValue($value) {
$this->hasValue = true;
$this->value = $value;
// If we don't have an initial value set yet, use the value as the
// initial value.
$initial_value = $this->getInitialValue();
if ($initial_value === null) {
$this->initialValue = $value;
}
return $this;
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadata() {
return $this->metadata;
}
public function getValueForTransaction() {
return $this->getValue();
}
public function getTransactionType() {
return $this->transactionType;
}
public function setTransactionType($type) {
$this->transactionType = $type;
return $this;
}
public function readValueFromRequest(AphrontRequest $request) {
$check = $this->getAllReadValueFromRequestKeys();
foreach ($check as $key) {
if (!$this->getValueExistsInRequest($request, $key)) {
continue;
}
$this->value = $this->getValueFromRequest($request, $key);
break;
}
return $this;
}
public function readValueFromComment($value) {
$this->value = $this->getValueFromComment($value);
return $this;
}
protected function getValueFromComment($value) {
return $value;
}
public function getAllReadValueFromRequestKeys() {
$keys = array();
$keys[] = $this->getKey();
foreach ($this->getAliases() as $alias) {
$keys[] = $alias;
}
return $keys;
}
public function readDefaultValueFromConfiguration($value) {
$this->value = $this->getDefaultValueFromConfiguration($value);
return $this;
}
protected function getDefaultValueFromConfiguration($value) {
return $value;
}
protected function getValueFromObject($object) {
if ($this->hasValue) {
return $this->value;
} else {
return $this->getDefaultValue();
}
}
protected function getValueExistsInRequest(AphrontRequest $request, $key) {
return $this->getHTTPParameterValueExists($request, $key);
}
protected function getValueFromRequest(AphrontRequest $request, $key) {
return $this->getHTTPParameterValue($request, $key);
}
public function readValueFromField(PhabricatorEditField $other) {
$this->value = $this->getValueFromField($other);
return $this;
}
protected function getValueFromField(PhabricatorEditField $other) {
return $other->getValue();
}
/**
* Read and return the value the object had when the user first loaded the
* form.
*
* This is the initial value from the user's point of view when they started
* the edit process, and used primarily to prevent race conditions for fields
* like "Projects" and "Subscribers" that use tokenizers and support edge
* transactions.
*
* Most fields do not need to store these values or deal with initial value
* handling.
*
* @param AphrontRequest Request to read from.
* @param string Key to read.
* @return wild Value read from request.
*/
protected function getInitialValueFromSubmit(AphrontRequest $request, $key) {
return null;
}
public function getInitialValue() {
return $this->initialValue;
}
public function setInitialValue($initial_value) {
$this->initialValue = $initial_value;
return $this;
}
public function readValueFromSubmit(AphrontRequest $request) {
$key = $this->getKey();
if ($this->getValueExistsInSubmit($request, $key)) {
$value = $this->getValueFromSubmit($request, $key);
} else {
$value = $this->getDefaultValue();
}
$this->value = $value;
$initial_value = $this->getInitialValueFromSubmit($request, $key);
$this->initialValue = $initial_value;
return $this;
}
protected function getValueExistsInSubmit(AphrontRequest $request, $key) {
return $this->getHTTPParameterValueExists($request, $key);
}
protected function getValueFromSubmit(AphrontRequest $request, $key) {
return $this->getHTTPParameterValue($request, $key);
}
protected function getHTTPParameterValueExists(
AphrontRequest $request,
$key) {
$type = $this->getHTTPParameterType();
if ($type) {
return $type->getExists($request, $key);
}
return false;
}
protected function getHTTPParameterValue($request, $key) {
$type = $this->getHTTPParameterType();
if ($type) {
return $type->getValue($request, $key);
}
return null;
}
protected function getDefaultValue() {
$type = $this->getHTTPParameterType();
if ($type) {
return $type->getDefaultValue();
}
return null;
}
final public function getHTTPParameterType() {
if ($this->getIsConduitOnly()) {
return null;
}
$type = $this->newHTTPParameterType();
if ($type) {
$type->setViewer($this->getViewer());
}
return $type;
}
protected function newHTTPParameterType() {
return new AphrontStringHTTPParameterType();
}
protected function getBulkParameterType() {
$type = $this->newBulkParameterType();
if (!$type) {
return null;
}
$type
->setField($this)
->setViewer($this->getViewer());
return $type;
}
protected function newBulkParameterType() {
return null;
}
public function getConduitParameterType() {
$type = $this->newConduitParameterType();
if (!$type) {
return null;
}
$type->setViewer($this->getViewer());
return $type;
}
abstract protected function newConduitParameterType();
public function setEditTypeKey($edit_type_key) {
$this->editTypeKey = $edit_type_key;
return $this;
}
public function getEditTypeKey() {
if ($this->editTypeKey === null) {
return $this->getKey();
}
return $this->editTypeKey;
}
protected function newEditType() {
return new PhabricatorSimpleEditType();
}
protected function getEditType() {
$transaction_type = $this->getTransactionType();
if ($transaction_type === null) {
return null;
}
$edit_type = $this->newEditType();
if (!$edit_type) {
return null;
}
$type_key = $this->getEditTypeKey();
$edit_type
->setEditField($this)
->setTransactionType($transaction_type)
->setEditType($type_key)
->setMetadata($this->getMetadata());
if (!$edit_type->getConduitParameterType()) {
$conduit_parameter = $this->getConduitParameterType();
if ($conduit_parameter) {
$edit_type->setConduitParameterType($conduit_parameter);
}
}
if (!$edit_type->getBulkParameterType()) {
$bulk_parameter = $this->getBulkParameterType();
if ($bulk_parameter) {
$edit_type->setBulkParameterType($bulk_parameter);
}
}
return $edit_type;
}
final public function getConduitEditTypes() {
if ($this->conduitEditTypes === null) {
$edit_types = $this->newConduitEditTypes();
$edit_types = mpull($edit_types, null, 'getEditType');
$this->conduitEditTypes = $edit_types;
}
return $this->conduitEditTypes;
}
final public function getConduitEditType($key) {
$edit_types = $this->getConduitEditTypes();
if (empty($edit_types[$key])) {
throw new Exception(
pht(
'This EditField does not provide a Conduit EditType with key "%s".',
$key));
}
return $edit_types[$key];
}
protected function newConduitEditTypes() {
$edit_type = $this->getEditType();
if (!$edit_type) {
return array();
}
return array($edit_type);
}
final public function getBulkEditTypes() {
if ($this->bulkEditTypes === null) {
$edit_types = $this->newBulkEditTypes();
$edit_types = mpull($edit_types, null, 'getEditType');
$this->bulkEditTypes = $edit_types;
}
return $this->bulkEditTypes;
}
final public function getBulkEditType($key) {
$edit_types = $this->getBulkEditTypes();
if (empty($edit_types[$key])) {
throw new Exception(
pht(
'This EditField does not provide a Bulk EditType with key "%s".',
$key));
}
return $edit_types[$key];
}
protected function newBulkEditTypes() {
$edit_type = $this->getEditType();
if (!$edit_type) {
return array();
}
return array($edit_type);
}
public function getCommentAction() {
$label = $this->getCommentActionLabel();
if ($label === null) {
return null;
}
$action = $this->newCommentAction();
if ($action === null) {
return null;
}
if ($this->hasCommentActionValue) {
$value = $this->getCommentActionValue();
} else {
$value = $this->getValue();
}
$action
->setKey($this->getKey())
->setLabel($label)
->setValue($this->getValueForCommentAction($value))
->setOrder($this->getCommentActionOrder())
->setGroupKey($this->getCommentActionGroupKey());
return $action;
}
protected function newCommentAction() {
return null;
}
protected function getValueForCommentAction($value) {
return $value;
}
public function shouldGenerateTransactionsFromSubmit() {
if ($this->getIsConduitOnly()) {
return false;
}
$edit_type = $this->getEditType();
if (!$edit_type) {
return false;
}
return true;
}
public function shouldReadValueFromRequest() {
if ($this->getIsConduitOnly()) {
return false;
}
if ($this->getIsLocked()) {
return false;
}
if ($this->getIsHidden()) {
return false;
}
return true;
}
public function shouldReadValueFromSubmit() {
if ($this->getIsConduitOnly()) {
return false;
}
if ($this->getIsLocked()) {
return false;
}
if ($this->getIsHidden()) {
return false;
}
return true;
}
public function shouldGenerateTransactionsFromComment() {
if ($this->getIsConduitOnly()) {
return false;
}
if ($this->getIsLocked()) {
return false;
}
if ($this->getIsHidden()) {
return false;
}
return true;
}
public function generateTransactions(
PhabricatorApplicationTransaction $template,
array $spec) {
$edit_type = $this->getEditType();
if (!$edit_type) {
throw new Exception(
pht(
'EditField (with key "%s", of class "%s") is generating '.
'transactions, but has no EditType.',
$this->getKey(),
get_class($this)));
}
return $edit_type->generateTransactions($template, $spec);
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 70644b197..fd801b810 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,4331 +1,4576 @@
<?php
/**
*
* Publishing and Managing State
* ======
*
* After applying changes, the Editor queues a worker to publish mail, feed,
* and notifications, and to perform other background work like updating search
* indexes. This allows it to do this work without impacting performance for
* users.
*
* When work is moved to the daemons, the Editor state is serialized by
* @{method:getWorkerState}, then reloaded in a daemon process by
* @{method:loadWorkerState}. **This is fragile.**
*
* State is not persisted into the daemons by default, because we can not send
* arbitrary objects into the queue. This means the default behavior of any
* state properties is to reset to their defaults without warning prior to
* publishing.
*
* The easiest way to avoid this is to keep Editors stateless: the overwhelming
* majority of Editors can be written statelessly. If you need to maintain
* state, you can either:
*
* - not require state to exist during publishing; or
* - pass state to the daemons by implementing @{method:getCustomWorkerState}
* and @{method:loadCustomWorkerState}.
*
* This architecture isn't ideal, and we may eventually split this class into
* "Editor" and "Publisher" parts to make it more robust. See T6367 for some
* discussion and context.
*
* @task mail Sending Mail
* @task feed Publishing Feed Stories
* @task search Search Index
* @task files Integration with Files
* @task workers Managing Workers
*/
abstract class PhabricatorApplicationTransactionEditor
extends PhabricatorEditor {
private $contentSource;
private $object;
private $xactions;
private $isNewObject;
private $mentionedPHIDs;
private $continueOnNoEffect;
private $continueOnMissingFields;
private $raiseWarnings;
private $parentMessageID;
private $heraldAdapter;
private $heraldTranscript;
private $subscribers;
private $unmentionablePHIDMap = array();
private $applicationEmail;
private $isPreview;
private $isHeraldEditor;
private $isInverseEdgeEditor;
private $actingAsPHID;
private $heraldEmailPHIDs = array();
private $heraldForcedEmailPHIDs = array();
private $heraldHeader;
private $mailToPHIDs = array();
private $mailCCPHIDs = array();
private $feedNotifyPHIDs = array();
private $feedRelatedPHIDs = array();
private $feedShouldPublish = false;
private $mailShouldSend = false;
private $modularTypes;
private $silent;
private $mustEncrypt;
private $stampTemplates = array();
private $mailStamps = array();
private $oldTo = array();
private $oldCC = array();
private $mailRemovedPHIDs = array();
private $mailUnexpandablePHIDs = array();
private $mailMutedPHIDs = array();
private $webhookMap = array();
private $transactionQueue = array();
+ private $sendHistory = false;
const STORAGE_ENCODING_BINARY = 'binary';
/**
* Get the class name for the application this editor is a part of.
*
* Uninstalling the application will disable the editor.
*
* @return string Editor's application class name.
*/
abstract public function getEditorApplicationClass();
/**
* Get a description of the objects this editor edits, like "Differential
* Revisions".
*
* @return string Human readable description of edited objects.
*/
abstract public function getEditorObjectsDescription();
public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
public function getActingAsPHID() {
if ($this->actingAsPHID) {
return $this->actingAsPHID;
}
return $this->getActor()->getPHID();
}
/**
* When the editor tries to apply transactions that have no effect, should
* it raise an exception (default) or drop them and continue?
*
* Generally, you will set this flag for edits coming from "Edit" interfaces,
* and leave it cleared for edits coming from "Comment" interfaces, so the
* user will get a useful error if they try to submit a comment that does
* nothing (e.g., empty comment with a status change that has already been
* performed by another user).
*
* @param bool True to drop transactions without effect and continue.
* @return this
*/
public function setContinueOnNoEffect($continue) {
$this->continueOnNoEffect = $continue;
return $this;
}
public function getContinueOnNoEffect() {
return $this->continueOnNoEffect;
}
/**
* When the editor tries to apply transactions which don't populate all of
* an object's required fields, should it raise an exception (default) or
* drop them and continue?
*
* For example, if a user adds a new required custom field (like "Severity")
* to a task, all existing tasks won't have it populated. When users
* manually edit existing tasks, it's usually desirable to have them provide
* a severity. However, other operations (like batch editing just the
* owner of a task) will fail by default.
*
* By setting this flag for edit operations which apply to specific fields
* (like the priority, batch, and merge editors in Maniphest), these
* operations can continue to function even if an object is outdated.
*
* @param bool True to continue when transactions don't completely satisfy
* all required fields.
* @return this
*/
public function setContinueOnMissingFields($continue_on_missing_fields) {
$this->continueOnMissingFields = $continue_on_missing_fields;
return $this;
}
public function getContinueOnMissingFields() {
return $this->continueOnMissingFields;
}
/**
* Not strictly necessary, but reply handlers ideally set this value to
* make email threading work better.
*/
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function getParentMessageID() {
return $this->parentMessageID;
}
public function getIsNewObject() {
return $this->isNewObject;
}
public function getMentionedPHIDs() {
return $this->mentionedPHIDs;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsSilent($silent) {
$this->silent = $silent;
return $this;
}
public function getIsSilent() {
return $this->silent;
}
public function getMustEncrypt() {
return $this->mustEncrypt;
}
public function getHeraldRuleMonograms() {
// Convert the stored "<123>, <456>" string into a list: "H123", "H456".
$list = $this->heraldHeader;
$list = preg_split('/[, ]+/', $list);
foreach ($list as $key => $item) {
$item = trim($item, '<>');
if (!is_numeric($item)) {
unset($list[$key]);
continue;
}
$list[$key] = 'H'.$item;
}
return $list;
}
public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
$this->isInverseEdgeEditor = $is_inverse_edge_editor;
return $this;
}
public function getIsInverseEdgeEditor() {
return $this->isInverseEdgeEditor;
}
public function setIsHeraldEditor($is_herald_editor) {
$this->isHeraldEditor = $is_herald_editor;
return $this;
}
public function getIsHeraldEditor() {
return $this->isHeraldEditor;
}
public function setUnmentionablePHIDMap(array $map) {
$this->unmentionablePHIDMap = $map;
return $this;
}
public function getUnmentionablePHIDMap() {
return $this->unmentionablePHIDMap;
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function setRaiseWarnings($raise_warnings) {
$this->raiseWarnings = $raise_warnings;
return $this;
}
public function getRaiseWarnings() {
return $this->raiseWarnings;
}
public function getTransactionTypesForObject($object) {
$old = $this->object;
try {
$this->object = $object;
$result = $this->getTransactionTypes();
$this->object = $old;
} catch (Exception $ex) {
$this->object = $old;
throw $ex;
}
return $result;
}
public function getTransactionTypes() {
$types = array();
$types[] = PhabricatorTransactions::TYPE_CREATE;
+ $types[] = PhabricatorTransactions::TYPE_HISTORY;
+
+ if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
+ $types[] = PhabricatorTransactions::TYPE_SUBTYPE;
+ }
if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBTYPE;
}
if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
}
if ($this->object instanceof PhabricatorCustomFieldInterface) {
$types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
if ($this->object instanceof PhabricatorTokenReceiverInterface) {
$types[] = PhabricatorTransactions::TYPE_TOKEN;
}
if ($this->object instanceof PhabricatorProjectInterface ||
$this->object instanceof PhabricatorMentionableInterface) {
$types[] = PhabricatorTransactions::TYPE_EDGE;
}
if ($this->object instanceof PhabricatorSpacesInterface) {
$types[] = PhabricatorTransactions::TYPE_SPACE;
}
$template = $this->object->getApplicationTransactionTemplate();
if ($template instanceof PhabricatorModularTransaction) {
$xtypes = $template->newModularTransactionTypes();
foreach ($xtypes as $xtype) {
$types[] = $xtype->getTransactionTypeConstant();
}
}
if ($template) {
try {
$comment = $template->getApplicationTransactionCommentObject();
} catch (PhutilMethodNotImplementedException $ex) {
$comment = null;
}
if ($comment) {
$types[] = PhabricatorTransactions::TYPE_COMMENT;
}
}
return $types;
}
private function adjustTransactionValues(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
if ($xaction->shouldGenerateOldValue()) {
$old = $this->getTransactionOldValue($object, $xaction);
$xaction->setOldValue($old);
}
$new = $this->getTransactionNewValue($object, $xaction);
$xaction->setNewValue($new);
}
private function getTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->generateOldValue($object);
}
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
+ case PhabricatorTransactions::TYPE_HISTORY:
return null;
case PhabricatorTransactions::TYPE_SUBTYPE:
return $object->getEditEngineSubtype();
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return array_values($this->subscribers);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getEditPolicy();
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getJoinPolicy();
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsNewObject()) {
return null;
}
$space_phid = $object->getSpacePHID();
if ($space_phid === null) {
$default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
if ($default_space) {
$space_phid = $default_space->getPHID();
}
}
return $space_phid;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if (!$edge_type) {
throw new Exception(
pht(
"Edge transaction has no '%s'!",
'edge:type'));
}
$old_edges = array();
if ($object->getPHID()) {
$edge_src = $object->getPHID();
$old_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($edge_src))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->execute();
$old_edges = $old_edges[$edge_src][$edge_type];
}
return $old_edges;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
// NOTE: Custom fields have their old value pre-populated when they are
// built by PhabricatorCustomFieldList.
return $xaction->getOldValue();
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionOldValue($object, $xaction);
}
}
private function getTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->generateNewValue($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->getPHIDTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SUBTYPE:
+ case PhabricatorTransactions::TYPE_HISTORY:
return $xaction->getNewValue();
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $xaction->getNewValue();
if (!strlen($space_phid)) {
// If an install has no Spaces or the Spaces controls are not visible
// to the viewer, we might end up with the empty string here instead
// of a strict `null`, because some controller just used `getStr()`
// to read the space PHID from the request.
// Just make this work like callers might reasonably expect so we
// don't need to handle this specially in every EditController.
return $this->getActor()->getDefaultSpacePHID();
} else {
return $space_phid;
}
case PhabricatorTransactions::TYPE_EDGE:
$new_value = $this->getEdgeTransactionNewValue($xaction);
$edge_type = $xaction->getMetadataValue('edge:type');
$type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
if ($edge_type == $type_project) {
$new_value = $this->applyProjectConflictRules($new_value);
}
return $new_value;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getNewValueFromApplicationTransactions($xaction);
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionNewValue($object, $xaction);
}
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception(pht('Capability not supported!'));
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception(pht('Capability not supported!'));
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
+ case PhabricatorTransactions::TYPE_HISTORY:
return true;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getApplicationTransactionHasEffect($xaction);
case PhabricatorTransactions::TYPE_EDGE:
// A straight value comparison here doesn't always get the right
// result, because newly added edges aren't fully populated. Instead,
// compare the changes in a more granular way.
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$old_dst = array_keys($old);
$new_dst = array_keys($new);
// NOTE: For now, we don't consider edge reordering to be a change.
// We have very few order-dependent edges and effectively no order
// oriented UI. This might change in the future.
sort($old_dst);
sort($new_dst);
if ($old_dst !== $new_dst) {
// We've added or removed edges, so this transaction definitely
// has an effect.
return true;
}
// We haven't added or removed edges, but we might have changed
// edge data.
foreach ($old as $key => $old_value) {
$new_value = $new[$key];
if ($old_value['data'] !== $new_value['data']) {
return true;
}
}
return false;
}
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getTransactionHasEffect(
$object,
$xaction->getOldValue(),
$xaction->getNewValue());
}
if ($xaction->hasComment()) {
return true;
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
throw new PhutilMethodNotImplementedException();
}
private function applyInternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->applyInternalEffects($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionInternalEffects($xaction);
case PhabricatorTransactions::TYPE_CREATE:
+ case PhabricatorTransactions::TYPE_HISTORY:
case PhabricatorTransactions::TYPE_SUBTYPE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinInternalTransaction($object, $xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->applyExternalEffects($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($object)
->setActor($this->requireActor());
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$subeditor->unsubscribe(
array_keys(
array_diff_key($old_map, $new_map)));
$subeditor->subscribeExplicit(
array_keys(
array_diff_key($new_map, $old_map)));
$subeditor->save();
// for the rest of these edits, subscribers should include those just
// added as well as those just removed.
$subscribers = array_unique(array_merge(
$this->subscribers,
$xaction->getOldValue(),
$xaction->getNewValue()));
$this->subscribers = $subscribers;
return $this->applyBuiltinExternalTransaction($object, $xaction);
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionExternalEffects($xaction);
case PhabricatorTransactions::TYPE_CREATE:
+ case PhabricatorTransactions::TYPE_HISTORY:
case PhabricatorTransactions::TYPE_SUBTYPE:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinExternalTransaction($object, $xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an internal apply implementation!",
$type));
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an external apply implementation!",
$type));
}
/**
* @{class:PhabricatorTransactions} provides many built-in transactions
* which should not require much - if any - code in specific applications.
*
* This method is a hook for the exceedingly-rare cases where you may need
* to do **additional** work for built-in transactions. Developers should
* extend this method, making sure to return the parent implementation
* regardless of handling any transactions.
*
* See also @{method:applyBuiltinExternalTransaction}.
*/
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_JOIN_POLICY:
$object->setJoinPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SPACE:
$object->setSpacePHID($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
$object->setEditEngineSubtype($xaction->getNewValue());
break;
}
}
/**
* See @{method::applyBuiltinInternalTransaction}.
*/
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
if ($this->getIsInverseEdgeEditor()) {
// If we're writing an inverse edge transaction, don't actually
// do anything. The initiating editor on the other side of the
// transaction will take care of the edge writes.
break;
}
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$src = $object->getPHID();
$const = $xaction->getMetadataValue('edge:type');
$type = PhabricatorEdgeType::getByConstant($const);
if ($type->shouldWriteInverseTransactions()) {
$this->applyInverseEdgeTransactions(
$object,
$xaction,
$type->getInverseEdgeConstant());
}
foreach ($new as $dst_phid => $edge) {
$new[$dst_phid]['src'] = $src;
}
$editor = new PhabricatorEdgeEditor();
foreach ($old as $dst_phid => $edge) {
if (!empty($new[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$editor->removeEdge($src, $const, $dst_phid);
}
foreach ($new as $dst_phid => $edge) {
if (!empty($old[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$data = array(
'data' => $edge['data'],
);
$editor->addEdge($src, $const, $dst_phid, $data);
}
$editor->save();
$this->updateWorkboardColumns($object, $const, $old, $new);
break;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
$this->scrambleFileSecrets($object);
break;
+ case PhabricatorTransactions::TYPE_HISTORY:
+ $this->sendHistory = true;
+ break;
}
}
/**
* Fill in a transaction's common values, like author and content source.
*/
protected function populateTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$actor = $this->getActor();
// TODO: This needs to be more sophisticated once we have meta-policies.
$xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
if ($actor->isOmnipotent()) {
$xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
} else {
$xaction->setEditPolicy($this->getActingAsPHID());
}
// If the transaction already has an explicit author PHID, allow it to
// stand. This is used by applications like Owners that hook into the
// post-apply change pipeline.
if (!$xaction->getAuthorPHID()) {
$xaction->setAuthorPHID($this->getActingAsPHID());
}
$xaction->setContentSource($this->getContentSource());
$xaction->attachViewer($actor);
$xaction->attachObject($object);
if ($object->getPHID()) {
$xaction->setObjectPHID($object->getPHID());
}
if ($this->getIsSilent()) {
$xaction->setIsSilentTransaction(true);
}
return $xaction;
}
protected function didApplyInternalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setContentSourceFromRequest(AphrontRequest $request) {
return $this->setContentSource(
PhabricatorContentSource::newFromRequest($request));
}
public function getContentSource() {
return $this->contentSource;
}
final public function applyTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($object, $xactions);
$actor = $this->requireActor();
// NOTE: Some transaction expansion requires that the edited object be
// attached.
foreach ($xactions as $xaction) {
$xaction->attachObject($object);
$xaction->attachViewer($actor);
}
$xactions = $this->expandTransactions($object, $xactions);
$xactions = $this->expandSupportTransactions($object, $xactions);
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
$xaction = $this->populateTransaction($object, $xaction);
}
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
if (!$is_preview) {
$errors = array();
$type_map = mgroup($xactions, 'getTransactionType');
foreach ($this->getTransactionTypes() as $type) {
$type_xactions = idx($type_map, $type, array());
$errors[] = $this->validateTransaction($object, $type, $type_xactions);
}
$errors[] = $this->validateAllTransactions($object, $xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
foreach ($errors as $key => $error) {
if ($continue_on_missing && $error->getIsMissingFieldError()) {
unset($errors[$key]);
}
}
if ($errors) {
throw new PhabricatorApplicationTransactionValidationException($errors);
}
if ($this->raiseWarnings) {
$warnings = array();
foreach ($xactions as $xaction) {
if ($this->hasWarnings($object, $xaction)) {
$warnings[] = $xaction;
}
}
if ($warnings) {
throw new PhabricatorApplicationTransactionWarningException(
$warnings);
}
}
$this->willApplyTransactions($object, $xactions);
if ($object->getID()) {
$this->buildOldRecipientLists($object, $xactions);
$object->openTransaction();
$transaction_open = true;
$object->beginReadLocking();
$read_locking = true;
$object->reload();
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
try {
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
+ // Now that we've merged and combined transactions, check for required
+ // capabilities. Note that we're doing this before filtering
+ // transactions: if you try to apply an edit which you do not have
+ // permission to apply, we want to give you a permissions error even
+ // if the edit would have no effect.
+ $this->applyCapabilityChecks($object, $xactions);
+
+ // See T13186. Fatal hard if this object has an older
+ // "requireCapabilities()" method. The code may rely on this method being
+ // called to apply policy checks, so err on the side of safety and fatal.
+ // TODO: Remove this check after some time has passed.
+ if (method_exists($this, 'requireCapabilities')) {
+ throw new Exception(
+ pht(
+ 'Editor (of class "%s") implements obsolete policy method '.
+ 'requireCapabilities(). The implementation for this Editor '.
+ 'MUST be updated. See <%s> for discussion.',
+ get_class($this),
+ 'https://secure.phabricator.com/T13186'));
+ }
+
$xactions = $this->filterTransactions($object, $xactions);
// TODO: Once everything is on EditEngine, just use getIsNewObject() to
// figure this out instead.
$mark_as_create = false;
$create_type = PhabricatorTransactions::TYPE_CREATE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $create_type) {
$mark_as_create = true;
}
}
if ($mark_as_create) {
foreach ($xactions as $xaction) {
$xaction->setIsCreateTransaction(true);
}
}
- // Now that we've merged, filtered, and combined transactions, check for
- // required capabilities.
- foreach ($xactions as $xaction) {
- $this->requireCapabilities($object, $xaction);
- }
-
$xactions = $this->sortTransactions($xactions);
$file_phids = $this->extractFilePHIDs($object, $xactions);
if ($is_preview) {
$this->loadHandles($xactions);
return $xactions;
}
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($actor)
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
foreach ($xactions as $xaction) {
$this->applyInternalEffects($object, $xaction);
}
$xactions = $this->didApplyInternalEffects($object, $xactions);
try {
$object->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// This callback has an opportunity to throw a better exception,
// so execution may end here.
$this->didCatchDuplicateKeyException($object, $xactions, $ex);
throw $ex;
}
foreach ($xactions as $xaction) {
$xaction->setObjectPHID($object->getPHID());
if ($xaction->getComment()) {
$xaction->setPHID($xaction->generatePHID());
$comment_editor->applyEdit($xaction, $xaction->getComment());
} else {
// TODO: This is a transitional hack to let us migrate edge
// transactions to a more efficient storage format. For now, we're
// going to write a new slim format to the database but keep the old
// bulky format on the objects so we don't have to upgrade all the
// edit logic to the new format yet. See T13051.
$edge_type = PhabricatorTransactions::TYPE_EDGE;
if ($xaction->getTransactionType() == $edge_type) {
$bulky_old = $xaction->getOldValue();
$bulky_new = $xaction->getNewValue();
$record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
$slim_old = $record->getModernOldEdgeTransactionData();
$slim_new = $record->getModernNewEdgeTransactionData();
$xaction->setOldValue($slim_old);
$xaction->setNewValue($slim_new);
$xaction->save();
$xaction->setOldValue($bulky_old);
$xaction->setNewValue($bulky_new);
} else {
$xaction->save();
}
}
}
if ($file_phids) {
$this->attachFiles($object, $file_phids);
}
foreach ($xactions as $xaction) {
$this->applyExternalEffects($object, $xaction);
}
$xactions = $this->applyFinalEffects($object, $xactions);
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->saveTransaction();
$transaction_open = false;
}
} catch (Exception $ex) {
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->killTransaction();
$transaction_open = false;
}
throw $ex;
}
// If we need to perform cache engine updates, execute them now.
id(new PhabricatorCacheEngine())
->updateObject($object);
// Now that we've completely applied the core transaction set, try to apply
// Herald rules. Herald rules are allowed to either take direct actions on
// the database (like writing flags), or take indirect actions (like saving
// some targets for CC when we generate mail a little later), or return
// transactions which we'll apply normally using another Editor.
// First, check if *this* is a sub-editor which is itself applying Herald
// rules: if it is, stop working and return so we don't descend into
// madness.
// Otherwise, we're not a Herald editor, so process Herald rules (possibly
// using a Herald editor to apply resulting transactions) and then send out
// mail, notifications, and feed updates about everything.
if ($this->getIsHeraldEditor()) {
// We are the Herald editor, so stop work here and return the updated
// transactions.
return $xactions;
} else if ($this->getIsInverseEdgeEditor()) {
// Do not run Herald if we're just recording that this object was
// mentioned elsewhere. This tends to create Herald side effects which
// feel arbitrary, and can really slow down edits which mention a large
// number of other objects. See T13114.
} else if ($this->shouldApplyHeraldRules($object, $xactions)) {
// We are not the Herald editor, so try to apply Herald rules.
$herald_xactions = $this->applyHeraldRules($object, $xactions);
if ($herald_xactions) {
$xscript_id = $this->getHeraldTranscript()->getID();
foreach ($herald_xactions as $herald_xaction) {
// Don't set a transcript ID if this is a transaction from another
// application or source, like Owners.
if ($herald_xaction->getAuthorPHID()) {
continue;
}
$herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
}
// NOTE: We're acting as the omnipotent user because rules deal with
// their own policy issues. We use a synthetic author PHID (the
// Herald application) as the author of record, so that transactions
// will render in a reasonable way ("Herald assigned this task ...").
$herald_actor = PhabricatorUser::getOmnipotentUser();
$herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
// TODO: It would be nice to give transactions a more specific source
// which points at the rule which generated them. You can figure this
// out from transcripts, but it would be cleaner if you didn't have to.
$herald_source = PhabricatorContentSource::newForSource(
PhabricatorHeraldContentSource::SOURCECONST);
$herald_editor = newv(get_class($this), array())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsHeraldEditor(true)
->setActor($herald_actor)
->setActingAsPHID($herald_phid)
->setContentSource($herald_source);
$herald_xactions = $herald_editor->applyTransactions(
$object,
$herald_xactions);
// Merge the new transactions into the transaction list: we want to
// send email and publish feed stories about them, too.
$xactions = array_merge($xactions, $herald_xactions);
}
// If Herald did not generate transactions, we may still need to handle
// "Send an Email" rules.
$adapter = $this->getHeraldAdapter();
$this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
$this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
$this->webhookMap = $adapter->getWebhookMap();
}
$xactions = $this->didApplyTransactions($object, $xactions);
if ($object instanceof PhabricatorCustomFieldInterface) {
// Maybe this makes more sense to move into the search index itself? For
// now I'm putting it here since I think we might end up with things that
// need it to be up to date once the next page loads, but if we don't go
// there we could move it into search once search moves to the daemons.
// It now happens in the search indexer as well, but the search indexer is
// always daemonized, so the logic above still potentially holds. We could
// possibly get rid of this. The major motivation for putting it in the
// indexer was to enable reindexing to work.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->readFieldsFromStorage($object);
$fields->rebuildIndexes($object);
}
$herald_xscript = $this->getHeraldTranscript();
if ($herald_xscript) {
$herald_header = $herald_xscript->getXHeraldRulesHeader();
$herald_header = HeraldTranscript::saveXHeraldRulesHeader(
$object->getPHID(),
$herald_header);
} else {
$herald_header = HeraldTranscript::loadXHeraldRulesHeader(
$object->getPHID());
}
$this->heraldHeader = $herald_header;
// We're going to compute some of the data we'll use to publish these
// transactions here, before queueing a worker.
//
// Primarily, this is more correct: we want to publish the object as it
// exists right now. The worker may not execute for some time, and we want
// to use the current To/CC list, not respect any changes which may occur
// between now and when the worker executes.
//
// As a secondary benefit, this tends to reduce the amount of state that
// Editors need to pass into workers.
$object = $this->willPublish($object, $xactions);
if (!$this->getIsSilent()) {
if ($this->shouldSendMail($object, $xactions)) {
$this->mailShouldSend = true;
$this->mailToPHIDs = $this->getMailTo($object);
$this->mailCCPHIDs = $this->getMailCC($object);
$this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object);
// Add any recipients who were previously on the notification list
// but were removed by this change.
$this->applyOldRecipientLists();
if ($object instanceof PhabricatorSubscribableInterface) {
$this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorMutedByEdgeType::EDGECONST);
} else {
$this->mailMutedPHIDs = array();
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$stamps = $this->newMailStamps($object, $xactions);
foreach ($stamps as $stamp) {
$this->mailStamps[] = $stamp->toDictionary();
}
}
if ($this->shouldPublishFeedStory($object, $xactions)) {
$this->feedShouldPublish = true;
$this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs(
$object,
$xactions);
$this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs(
$object,
$xactions);
}
}
PhabricatorWorker::scheduleTask(
'PhabricatorApplicationTransactionPublishWorker',
array(
'objectPHID' => $object->getPHID(),
'actorPHID' => $this->getActingAsPHID(),
'xactionPHIDs' => mpull($xactions, 'getPHID'),
'state' => $this->getWorkerState(),
),
array(
'objectPHID' => $object->getPHID(),
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
$this->flushTransactionQueue($object);
return $xactions;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
return;
}
public function publishTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
// Hook for edges or other properties that may need (re-)loading
$object = $this->willPublish($object, $xactions);
// The object might have changed, so reassign it.
$this->object = $object;
$messages = array();
if ($this->mailShouldSend) {
$messages = $this->buildMail($object, $xactions);
}
if ($this->supportsSearch()) {
PhabricatorSearchWorker::queueDocumentForIndexing(
$object->getPHID(),
array(
'transactionPHIDs' => mpull($xactions, 'getPHID'),
));
}
if ($this->feedShouldPublish) {
$mailed = array();
foreach ($messages as $mail) {
foreach ($mail->buildRecipientList() as $phid) {
$mailed[$phid] = $phid;
}
}
$this->publishFeedStory($object, $xactions, $mailed);
}
+ if ($this->sendHistory) {
+ $history_mail = $this->buildHistoryMail($object);
+ if ($history_mail) {
+ $messages[] = $history_mail;
+ }
+ }
+
// NOTE: This actually sends the mail. We do this last to reduce the chance
// that we send some mail, hit an exception, then send the mail again when
// retrying.
foreach ($messages as $mail) {
$mail->save();
}
$this->queueWebhooks($object, $xactions);
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
// Hook for subclasses.
return $xactions;
}
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($merged)
->execute();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht('You can not apply transactions which already have IDs/PHIDs!'));
}
if ($xaction->getObjectPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'objectPHIDs'));
}
if ($xaction->getCommentPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'commentPHIDs'));
}
if ($xaction->getCommentVersion() !== 0) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentVersions!'));
}
$expect_value = !$xaction->shouldGenerateOldValue();
$has_value = $xaction->hasOldValue();
if ($expect_value && !$has_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction is supposed to have an %s set, but it does not!',
'oldValue'));
}
if ($has_value && !$expect_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction should generate its %s automatically, '.
'but has already had one set!',
'oldValue'));
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'Transaction has type "%s", but that transaction type is not '.
'supported by this editor (%s).',
$type,
get_class($this)));
}
}
}
- protected function requireCapabilities(
+ private function applyCapabilityChecks(
PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
+ array $xactions) {
+ assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
+
+ $can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($this->getIsNewObject()) {
- return;
+ // If we're creating a new object, we don't need any special capabilities
+ // on the object. The actor has already made it through creation checks,
+ // and objects which haven't been created yet often can not be
+ // meaningfully tested for capabilities anyway.
+ $required_capabilities = array();
+ } else {
+ if (!$xactions && !$this->xactions) {
+ // If we aren't doing anything, require CAN_EDIT to improve consistency.
+ $required_capabilities = array($can_edit);
+ } else {
+ $required_capabilities = array();
+
+ foreach ($xactions as $xaction) {
+ $type = $xaction->getTransactionType();
+
+ $xtype = $this->getModularTransactionType($type);
+ if (!$xtype) {
+ $capabilities = $this->getLegacyRequiredCapabilities($xaction);
+ } else {
+ $capabilities = $xtype->getRequiredCapabilities($object, $xaction);
+ }
+
+ // For convenience, we allow flexibility in the return types because
+ // it's very unusual that a transaction actually requires multiple
+ // capability checks.
+ if ($capabilities === null) {
+ $capabilities = array();
+ } else {
+ $capabilities = (array)$capabilities;
+ }
+
+ foreach ($capabilities as $capability) {
+ $required_capabilities[$capability] = $capability;
+ }
+ }
+ }
}
- $actor = $this->requireActor();
- switch ($xaction->getTransactionType()) {
+ $required_capabilities = array_fuse($required_capabilities);
+ $actor = $this->getActor();
+
+ if ($required_capabilities) {
+ id(new PhabricatorPolicyFilter())
+ ->setViewer($actor)
+ ->requireCapabilities($required_capabilities)
+ ->raisePolicyExceptions(true)
+ ->apply(array($object));
+ }
+ }
+
+ private function getLegacyRequiredCapabilities(
+ PhabricatorApplicationTransaction $xaction) {
+
+ $type = $xaction->getTransactionType();
+ switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
- PhabricatorPolicyFilter::requireCapability(
- $actor,
- $object,
- PhabricatorPolicyCapability::CAN_VIEW);
- break;
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- case PhabricatorTransactions::TYPE_EDIT_POLICY:
- case PhabricatorTransactions::TYPE_JOIN_POLICY:
- case PhabricatorTransactions::TYPE_SPACE:
- PhabricatorPolicyFilter::requireCapability(
- $actor,
- $object,
- PhabricatorPolicyCapability::CAN_EDIT);
- break;
+ // TODO: Comments technically require CAN_INTERACT, but this is
+ // currently somewhat special and handled through EditEngine. For now,
+ // don't enforce it here.
+ return null;
+ case PhabricatorTransactions::TYPE_SUBSCRIBERS:
+ // TODO: Removing subscribers other than yourself should probably
+ // require CAN_EDIT permission. You can do this via the API but
+ // generally can not via the web interface.
+ return null;
+ case PhabricatorTransactions::TYPE_TOKEN:
+ // TODO: This technically requires CAN_INTERACT, like comments.
+ return null;
+ case PhabricatorTransactions::TYPE_HISTORY:
+ // This is a special magic transaction which sends you history via
+ // email and is only partially supported in the upstream. You don't
+ // need any capabilities to apply it.
+ return null;
+ case PhabricatorTransactions::TYPE_EDGE:
+ return $this->getLegacyRequiredEdgeCapabilities($xaction);
+ default:
+ // For other older (non-modular) transactions, always require exactly
+ // CAN_EDIT. Transactions which do not need CAN_EDIT or need additional
+ // capabilities must move to ModularTransactions.
+ return PhabricatorPolicyCapability::CAN_EDIT;
}
}
+ private function getLegacyRequiredEdgeCapabilities(
+ PhabricatorApplicationTransaction $xaction) {
+
+ // You don't need to have edit permission on an object to mention it or
+ // otherwise add a relationship pointing toward it.
+ if ($this->getIsInverseEdgeEditor()) {
+ return null;
+ }
+
+ $edge_type = $xaction->getMetadataValue('edge:type');
+ switch ($edge_type) {
+ case PhabricatorMutedByEdgeType::EDGECONST:
+ // At time of writing, you can only write this edge for yourself, so
+ // you don't need permissions. If you can eventually mute an object
+ // for other users, this would need to be revisited.
+ return null;
+ case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
+ return null;
+ case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
+ $old = $xaction->getOldValue();
+ $new = $xaction->getNewValue();
+
+ $add = array_keys(array_diff_key($new, $old));
+ $rem = array_keys(array_diff_key($old, $new));
+
+ $actor_phid = $this->requireActor()->getPHID();
+
+ $is_join = (($add === array($actor_phid)) && !$rem);
+ $is_leave = (($rem === array($actor_phid)) && !$add);
+
+ if ($is_join) {
+ // You need CAN_JOIN to join a project.
+ return PhabricatorPolicyCapability::CAN_JOIN;
+ }
+
+ if ($is_leave) {
+ $object = $this->object;
+ // You usually don't need any capabilities to leave a project...
+ if ($object->getIsMembershipLocked()) {
+ // ...you must be able to edit to leave locked projects, though.
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ } else {
+ return null;
+ }
+ }
+
+ // You need CAN_EDIT to change members other than yourself.
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ default:
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ }
+ }
+
+
private function buildSubscribeTransaction(
PhabricatorLiskDAO $object,
array $xactions,
array $changes) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
return null;
}
if ($this->shouldEnableMentions($object, $xactions)) {
// Identify newly mentioned users. We ignore users who were previously
// mentioned so that we don't re-subscribe users after an edit of text
// which mentions them.
$old_texts = mpull($changes, 'getOldValue');
$new_texts = mpull($changes, 'getNewValue');
$old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$old_texts);
$new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$new_texts);
$phids = array_diff($new_phids, $old_phids);
} else {
$phids = array();
}
$this->mentionedPHIDs = $phids;
if ($object->getPHID()) {
// Don't try to subscribe already-subscribed mentions: we want to generate
// a dialog about an action having no effect if the user explicitly adds
// existing CCs, but not if they merely mention existing subscribers.
$phids = array_diff($phids, $this->subscribers);
}
if ($phids) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($phids as $key => $phid) {
// Do not subscribe mentioned users
// who do not have VIEW Permissions
if ($object instanceof PhabricatorPolicyInterface
&& !PhabricatorPolicyFilter::hasCapability(
$users[$phid],
$object,
PhabricatorPolicyCapability::CAN_VIEW)
) {
unset($phids[$key]);
} else {
if ($object->isAutomaticallySubscribed($phid)) {
unset($phids[$key]);
}
}
}
$phids = array_values($phids);
}
// No else here to properly return null should we unset all subscriber
if (!$phids) {
return null;
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => $phids));
return $xaction;
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$object = $this->object;
return $xtype->mergeTransactions($object, $u, $v);
}
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PhabricatorTransactions::TYPE_EDGE:
$u_type = $u->getMetadataValue('edge:type');
$v_type = $v->getMetadataValue('edge:type');
if ($u_type == $v_type) {
return $this->mergePHIDOrEdgeTransactions($u, $v);
}
return null;
}
// By default, do not merge the transactions.
return null;
}
/**
* Optionally expand transactions which imply other effects. For example,
* resigning from a revision in Differential implies removing yourself as
* a reviewer.
*/
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$results = array();
foreach ($xactions as $xaction) {
foreach ($this->expandTransaction($object, $xaction) as $expanded) {
$results[] = $expanded;
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array($xaction);
}
public function getExpandedSupportTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = array($xaction);
$xactions = $this->expandSupportTransactions(
$object,
$xactions);
if (count($xactions) == 1) {
return array();
}
foreach ($xactions as $index => $cxaction) {
if ($cxaction === $xaction) {
unset($xactions[$index]);
break;
}
}
return $xactions;
}
private function expandSupportTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->loadSubscribers($object);
$xactions = $this->applyImplicitCC($object, $xactions);
$changes = $this->getRemarkupChanges($xactions);
$subscribe_xaction = $this->buildSubscribeTransaction(
$object,
$xactions,
$changes);
if ($subscribe_xaction) {
$xactions[] = $subscribe_xaction;
}
// TODO: For now, this is just a placeholder.
$engine = PhabricatorMarkupEngine::getEngine('extract');
$engine->setConfig('viewer', $this->requireActor());
$block_xactions = $this->expandRemarkupBlockTransactions(
$object,
$xactions,
$changes,
$engine);
foreach ($block_xactions as $xaction) {
$xactions[] = $xaction;
}
return $xactions;
}
private function getRemarkupChanges(array $xactions) {
$changes = array();
foreach ($xactions as $key => $xaction) {
foreach ($this->getRemarkupChangesFromTransaction($xaction) as $change) {
$changes[] = $change;
}
}
return $changes;
}
private function getRemarkupChangesFromTransaction(
PhabricatorApplicationTransaction $transaction) {
return $transaction->getRemarkupChanges();
}
private function expandRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
$block_xactions = $this->expandCustomRemarkupBlockTransactions(
$object,
$xactions,
$changes,
$engine);
$mentioned_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($changes as $change) {
// Here, we don't care about processing only new mentions after an edit
// because there is no way for an object to ever "unmention" itself on
// another object, so we can ignore the old value.
$engine->markupText($change->getNewValue());
$mentioned_phids += $engine->getTextMetadata(
PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
array());
}
}
if (!$mentioned_phids) {
return $block_xactions;
}
$mentioned_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($mentioned_phids)
->execute();
$mentionable_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($mentioned_objects as $mentioned_object) {
if ($mentioned_object instanceof PhabricatorMentionableInterface) {
$mentioned_phid = $mentioned_object->getPHID();
if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
continue;
}
// don't let objects mention themselves
if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
continue;
}
$mentionable_phids[$mentioned_phid] = $mentioned_phid;
}
}
}
if ($mentionable_phids) {
$edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$block_xactions[] = newv(get_class(head($xactions)), array())
->setIgnoreOnNoEffect(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array('+' => $mentionable_phids));
}
return $block_xactions;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
return array();
}
/**
* Attempt to combine similar transactions into a smaller number of total
* transactions. For example, two transactions which edit the title of an
* object can be merged into a single edit.
*/
private function combineTransactions(array $xactions) {
$stray_comments = array();
$result = array();
$types = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction->getTransactionType();
if (isset($types[$type])) {
foreach ($types[$type] as $other_key) {
$other_xaction = $result[$other_key];
// Don't merge transactions with different authors. For example,
// don't merge Herald transactions and owners transactions.
if ($other_xaction->getAuthorPHID() != $xaction->getAuthorPHID()) {
continue;
}
$merged = $this->mergeTransactions($result[$other_key], $xaction);
if ($merged) {
$result[$other_key] = $merged;
if ($xaction->getComment() &&
($xaction->getComment() !== $merged->getComment())) {
$stray_comments[] = $xaction->getComment();
}
if ($result[$other_key]->getComment() &&
($result[$other_key]->getComment() !== $merged->getComment())) {
$stray_comments[] = $result[$other_key]->getComment();
}
// Move on to the next transaction.
continue 2;
}
}
}
$result[$key] = $xaction;
$types[$type][] = $key;
}
// If we merged any comments away, restore them.
foreach ($stray_comments as $comment) {
$xaction = newv(get_class(head($result)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->setComment($comment);
$result[] = $xaction;
}
return array_values($result);
}
public function mergePHIDOrEdgeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$result = $u->getNewValue();
foreach ($v->getNewValue() as $key => $value) {
if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
if (empty($result[$key])) {
$result[$key] = $value;
} else {
// We're merging two lists of edge adds, sets, or removes. Merge
// them by merging individual PHIDs within them.
$merged = $result[$key];
foreach ($value as $dst => $v_spec) {
if (empty($merged[$dst])) {
$merged[$dst] = $v_spec;
} else {
// Two transactions are trying to perform the same operation on
// the same edge. Normalize the edge data and then merge it. This
// allows transactions to specify how data merges execute in a
// precise way.
$u_spec = $merged[$dst];
if (!is_array($u_spec)) {
$u_spec = array('dst' => $u_spec);
}
if (!is_array($v_spec)) {
$v_spec = array('dst' => $v_spec);
}
$ux_data = idx($u_spec, 'data', array());
$vx_data = idx($v_spec, 'data', array());
$merged_data = $this->mergeEdgeData(
$u->getMetadataValue('edge:type'),
$ux_data,
$vx_data);
$u_spec['data'] = $merged_data;
$merged[$dst] = $u_spec;
}
}
$result[$key] = $merged;
}
} else {
$result[$key] = array_merge($value, idx($result, $key, array()));
}
}
$u->setNewValue($result);
// When combining an "ignore" transaction with a normal transaction, make
// sure we don't propagate the "ignore" flag.
if (!$v->getIgnoreOnNoEffect()) {
$u->setIgnoreOnNoEffect(false);
}
return $u;
}
protected function mergeEdgeData($type, array $u, array $v) {
return $v + $u;
}
protected function getPHIDTransactionNewValue(
PhabricatorApplicationTransaction $xaction,
$old = null) {
if ($old !== null) {
$old = array_fuse($old);
} else {
$old = array_fuse($xaction->getOldValue());
}
return $this->getPHIDList($old, $xaction->getNewValue());
}
public function getPHIDList(array $old, array $new) {
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
if ($new_set !== null) {
$new_set = array_fuse($new_set);
}
unset($new['=']);
if ($new) {
throw new Exception(
pht(
"Invalid '%s' value for PHID transaction. Value should contain only ".
"keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).",
'new',
'+',
'-',
'='));
}
$result = array();
foreach ($old as $phid) {
if ($new_set !== null && empty($new_set[$phid])) {
continue;
}
$result[$phid] = $phid;
}
if ($new_set !== null) {
foreach ($new_set as $phid) {
$result[$phid] = $phid;
}
}
foreach ($new_add as $phid) {
$result[$phid] = $phid;
}
foreach ($new_rem as $phid) {
unset($result[$phid]);
}
return array_values($result);
}
protected function getEdgeTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
unset($new['=']);
if ($new) {
throw new Exception(
pht(
"Invalid '%s' value for Edge transaction. Value should contain only ".
"keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).",
'new',
'+',
'-',
'='));
}
$old = $xaction->getOldValue();
$lists = array($new_set, $new_add, $new_rem);
foreach ($lists as $list) {
$this->checkEdgeList($list, $xaction->getMetadataValue('edge:type'));
}
$result = array();
foreach ($old as $dst_phid => $edge) {
if ($new_set !== null && empty($new_set[$dst_phid])) {
continue;
}
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
if ($new_set !== null) {
foreach ($new_set as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
}
foreach ($new_add as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
foreach ($new_rem as $dst_phid => $edge) {
unset($result[$dst_phid]);
}
return $result;
}
private function checkEdgeList($list, $edge_type) {
if (!$list) {
return;
}
foreach ($list as $key => $item) {
if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
pht(
'Edge transactions must have destination PHIDs as in edge '.
'lists (found key "%s" on transaction of type "%s").',
$key,
$edge_type));
}
if (!is_array($item) && $item !== $key) {
throw new Exception(
pht(
'Edge transactions must have PHIDs or edge specs as values '.
'(found value "%s" on transaction of type "%s").',
$item,
$edge_type));
}
}
}
private function normalizeEdgeTransactionValue(
PhabricatorApplicationTransaction $xaction,
$edge,
$dst_phid) {
if (!is_array($edge)) {
if ($edge != $dst_phid) {
throw new Exception(
pht(
'Transaction edge data must either be the edge PHID or an edge '.
'specification dictionary.'));
}
$edge = array();
} else {
foreach ($edge as $key => $value) {
switch ($key) {
case 'src':
case 'dst':
case 'type':
case 'data':
case 'dateCreated':
case 'dateModified':
case 'seq':
case 'dataID':
break;
default:
throw new Exception(
pht(
'Transaction edge specification contains unexpected key "%s".',
$key));
}
}
}
$edge['dst'] = $dst_phid;
$edge_type = $xaction->getMetadataValue('edge:type');
if (empty($edge['type'])) {
$edge['type'] = $edge_type;
} else {
if ($edge['type'] != $edge_type) {
$this_type = $edge['type'];
throw new Exception(
pht(
"Edge transaction includes edge of type '%s', but ".
"transaction is of type '%s'. Each edge transaction ".
"must alter edges of only one type.",
$this_type,
$edge_type));
}
}
if (!isset($edge['data'])) {
$edge['data'] = array();
}
return $edge;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move bare comments to the end, so the actions precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
$no_effect = array();
$has_comment = false;
$any_effect = false;
foreach ($xactions as $key => $xaction) {
if ($this->transactionHasEffect($object, $xaction)) {
if ($xaction->getTransactionType() != $type_comment) {
$any_effect = true;
}
} else if ($xaction->getIgnoreOnNoEffect()) {
unset($xactions[$key]);
} else {
$no_effect[$key] = $xaction;
}
if ($xaction->hasComment()) {
$has_comment = true;
}
}
if (!$no_effect) {
return $xactions;
}
if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
throw new PhabricatorApplicationTransactionNoEffectException(
$no_effect,
$any_effect,
$has_comment);
}
if (!$any_effect && !$has_comment) {
// If we only have empty comment transactions, just drop them all.
return array();
}
foreach ($no_effect as $key => $xaction) {
if ($xaction->hasComment()) {
$xaction->setTransactionType($type_comment);
$xaction->setOldValue(null);
$xaction->setNewValue(null);
} else {
unset($xactions[$key]);
}
}
return $xactions;
}
/**
* Hook for validating transactions. This callback will be invoked for each
* available transaction type, even if an edit does not apply any transactions
* of that type. This allows you to raise exceptions when required fields are
* missing, by detecting that the object has no field value and there is no
* transaction which sets one.
*
* @param PhabricatorLiskDAO Object being edited.
* @param string Transaction type to validate.
* @param list<PhabricatorApplicationTransaction> Transactions of given type,
* which may be empty if the edit does not apply any transactions of the
* given type.
* @return list<PhabricatorApplicationTransactionValidationError> List of
* validation errors.
*/
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = array();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$errors[] = $xtype->validateTransactions($object, $xactions);
}
switch ($type) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_VIEW);
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorTransactions::TYPE_SPACE:
$errors[] = $this->validateSpaceTransactions(
$object,
$xactions,
$type);
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
$errors[] = $this->validateSubtypeTransactions(
$object,
$xactions,
$type);
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$groups = array();
foreach ($xactions as $xaction) {
$groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($this->getActor());
$role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($field_list->getFields() as $field) {
if (!$field->shouldEnableForRole($role_xactions)) {
continue;
}
$errors[] = $field->validateApplicationTransactions(
$this,
$type,
idx($groups, $field->getFieldKey(), array()));
}
break;
}
return array_mergev($errors);
}
public function validatePolicyTransaction(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type,
$capability) {
$actor = $this->requireActor();
$errors = array();
// Note $this->xactions is necessary; $xactions is $this->xactions of
// $transaction_type
$policy_object = $this->adjustObjectForPolicyChecks(
$object,
$this->xactions);
// Make sure the user isn't editing away their ability to $capability this
// object.
foreach ($xactions as $xaction) {
try {
PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
$actor,
$policy_object,
$capability,
$xaction->getNewValue());
} catch (PhabricatorPolicyException $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not select this %s policy, because you would no longer '.
'be able to %s the object.',
$capability,
$capability),
$xaction);
}
}
if ($this->getIsNewObject()) {
if (!$xactions) {
$has_capability = PhabricatorPolicyFilter::hasCapability(
$actor,
$policy_object,
$capability);
if (!$has_capability) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'The selected %s policy excludes you. Choose a %s policy '.
'which allows you to %s the object.',
$capability,
$capability,
$capability));
}
}
}
return $errors;
}
private function validateSpaceTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$actor = $this->getActor();
$has_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($actor);
$actor_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($actor);
$active_spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces(
$actor);
foreach ($xactions as $xaction) {
$space_phid = $xaction->getNewValue();
if ($space_phid === null) {
if (!$has_spaces) {
// The install doesn't have any spaces, so this is fine.
continue;
}
// The install has some spaces, so every object needs to be put
// in a valid space.
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht('You must choose a space for this object.'),
$xaction);
continue;
}
// If the PHID isn't `null`, it needs to be a valid space that the
// viewer can see.
if (empty($actor_spaces[$space_phid])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not shift this object in the selected space, because '.
'the space does not exist or you do not have access to it.'),
$xaction);
} else if (empty($active_spaces[$space_phid])) {
// It's OK to edit objects in an archived space, so just move on if
// we aren't adjusting the value.
$old_space_phid = $this->getTransactionOldValue($object, $xaction);
if ($space_phid == $old_space_phid) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Archived'),
pht(
'You can not shift this object into the selected space, because '.
'the space is archived. Objects can not be created inside (or '.
'moved into) archived spaces.'),
$xaction);
}
}
return $errors;
}
private function validateSubtypeTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$map = $object->newEditEngineSubtypeMap();
$old = $object->getEditEngineSubtype();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if ($old == $new) {
continue;
}
if (!isset($map[$new])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'The subtype "%s" is not a valid subtype.',
$new),
$xaction);
continue;
}
}
return $errors;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = clone $object;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$clone_xaction = clone $xaction;
$clone_xaction->setOldValue(array_values($this->subscribers));
$clone_xaction->setNewValue(
$this->getPHIDTransactionNewValue(
$clone_xaction));
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
new PhabricatorSubscriptionsSubscribersPolicyRule(),
array_fuse($clone_xaction->getNewValue()));
break;
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $this->getTransactionNewValue($object, $xaction);
$copy->setSpacePHID($space_phid);
break;
}
}
return $copy;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
return array();
}
/**
* Check for a missing text field.
*
* A text field is missing if the object has no value and there are no
* transactions which set a value, or if the transactions remove the value.
* This method is intended to make implementing @{method:validateTransaction}
* more convenient:
*
* $missing = $this->validateIsEmptyTextField(
* $object->getName(),
* $xactions);
*
* This will return `true` if the net effect of the object and transactions
* is an empty field.
*
* @param wild Current field value.
* @param list<PhabricatorApplicationTransaction> Transactions editing the
* field.
* @return bool True if the field will be an empty text field after edits.
*/
protected function validateIsEmptyTextField($field_value, array $xactions) {
if (strlen($field_value) && empty($xactions)) {
return false;
}
if ($xactions && strlen(last($xactions)->getNewValue())) {
return false;
}
return true;
}
/* -( Implicit CCs )------------------------------------------------------- */
/**
* When a user interacts with an object, we might want to add them to CC.
*/
final public function applyImplicitCC(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
// If the object isn't subscribable, we can't CC them.
return $xactions;
}
$actor_phid = $this->getActingAsPHID();
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($actor_phid) != $type_user) {
// Transactions by application actors like Herald, Harbormaster and
// Diffusion should not CC the applications.
return $xactions;
}
if ($object->isAutomaticallySubscribed($actor_phid)) {
// If they're auto-subscribed, don't CC them.
return $xactions;
}
$should_cc = false;
foreach ($xactions as $xaction) {
if ($this->shouldImplyCC($object, $xaction)) {
$should_cc = true;
break;
}
}
if (!$should_cc) {
// Only some types of actions imply a CC (like adding a comment).
return $xactions;
}
if ($object->getPHID()) {
if (isset($this->subscribers[$actor_phid])) {
// If the user is already subscribed, don't implicitly CC them.
return $xactions;
}
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
$unsub = array_fuse($unsub);
if (isset($unsub[$actor_phid])) {
// If the user has previously unsubscribed from this object explicitly,
// don't implicitly CC them.
return $xactions;
}
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => array($actor_phid)));
array_unshift($xactions, $xaction);
return $xactions;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return $xaction->isCommentTransaction();
}
/* -( Sending Mail )------------------------------------------------------- */
/**
* @task mail
*/
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task mail
*/
private function buildMail(
PhabricatorLiskDAO $object,
array $xactions) {
$email_to = $this->mailToPHIDs;
$email_cc = $this->mailCCPHIDs;
$email_cc = array_merge($email_cc, $this->heraldEmailPHIDs);
$unexpandable = $this->mailUnexpandablePHIDs;
if (!is_array($unexpandable)) {
$unexpandable = array();
}
+ $messages = $this->buildMailWithRecipients(
+ $object,
+ $xactions,
+ $email_to,
+ $email_cc,
+ $unexpandable);
+
+ $this->runHeraldMailRules($messages);
+
+ return $messages;
+ }
+
+ private function buildMailWithRecipients(
+ PhabricatorLiskDAO $object,
+ array $xactions,
+ array $email_to,
+ array $email_cc,
+ array $unexpandable) {
+
$targets = $this->buildReplyHandler($object)
->setUnexpandablePHIDs($unexpandable)
->getMailTargets($email_to, $email_cc);
// Set this explicitly before we start swapping out the effective actor.
$this->setActingAsPHID($this->getActingAsPHID());
$messages = array();
foreach ($targets as $target) {
$original_actor = $this->getActor();
$viewer = $target->getViewer();
$this->setActor($viewer);
$locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation());
$caught = null;
$mail = null;
try {
// Reload handles for the new viewer.
$this->loadHandles($xactions);
$mail = $this->buildMailForTarget($object, $xactions, $target);
if ($mail) {
if ($this->mustEncrypt) {
$mail
->setMustEncrypt(true)
->setMustEncryptReasons($this->mustEncrypt);
}
}
} catch (Exception $ex) {
$caught = $ex;
}
$this->setActor($original_actor);
unset($locale);
if ($caught) {
throw $ex;
}
if ($mail) {
$messages[] = $mail;
}
}
- $this->runHeraldMailRules($messages);
-
return $messages;
}
protected function getTransactionsForMail(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
private function buildMailForTarget(
PhabricatorLiskDAO $object,
array $xactions,
PhabricatorMailTarget $target) {
// Check if any of the transactions are visible for this viewer. If we
// don't have any visible transactions, don't send the mail.
$any_visible = false;
foreach ($xactions as $xaction) {
if (!$xaction->shouldHideForMail($xactions)) {
$any_visible = true;
break;
}
}
if (!$any_visible) {
return null;
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$mail = $this->buildMailTemplate($object);
$body = $this->buildMailBody($object, $mail_xactions);
$mail_tags = $this->getMailTags($object, $mail_xactions);
$action = $this->getMailAction($object, $mail_xactions);
$stamps = $this->generateMailStamps($object, $this->mailStamps);
if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) {
$this->addEmailPreferenceSectionToMailBody(
$body,
$object,
$mail_xactions);
}
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$mail
->setSensitiveContent(false)
->setFrom($this->getActingAsPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMutedPHIDs($muted_phids)
->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs)
->setMailTags($mail_tags)
->setIsBulk(true)
->setBody($body->render())
->setHTMLBody($body->renderHTML());
foreach ($body->getAttachments() as $attachment) {
$mail->addAttachment($attachment);
}
if ($this->heraldHeader) {
$mail->addHeader('X-Herald-Rules', $this->heraldHeader);
}
if ($object instanceof PhabricatorProjectInterface) {
$this->addMailProjectMetadata($object, $mail);
}
if ($this->getParentMessageID()) {
$mail->setParentMessageID($this->getParentMessageID());
}
// If we have stamps, attach the raw dictionary version (not the actual
// objects) to the mail so that debugging tools can see what we used to
// render the final list.
if ($this->mailStamps) {
$mail->setMailStampMetadata($this->mailStamps);
}
// If we have rendered stamps, attach them to the mail.
if ($stamps) {
$mail->setMailStamps($stamps);
}
return $target->willSendMail($mail);
}
private function addMailProjectMetadata(
PhabricatorLiskDAO $object,
PhabricatorMetaMTAMail $template) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if (!$project_phids) {
return;
}
// TODO: This viewer isn't quite right. It would be slightly better to use
// the mail recipient, but that's not very easy given the way rendering
// works today.
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($project_phids)
->execute();
$project_tags = array();
foreach ($handles as $handle) {
if (!$handle->isComplete()) {
continue;
}
$project_tags[] = '<'.$handle->getObjectName().'>';
}
if (!$project_tags) {
return;
}
$project_tags = implode(', ', $project_tags);
$template->addHeader('X-Phabricator-Projects', $project_tags);
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return $object->getPHID();
}
/**
* @task mail
*/
protected function getStrongestAction(
PhabricatorLiskDAO $object,
array $xactions) {
return last(msort($xactions, 'getActionStrength'));
}
/**
* @task mail
*/
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailSubjectPrefix() {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTags(
PhabricatorLiskDAO $object,
array $xactions) {
$tags = array();
foreach ($xactions as $xaction) {
$tags[] = $xaction->getMailTags();
}
return array_mergev($tags);
}
/**
* @task mail
*/
public function getMailTagsMap() {
// TODO: We should move shared mail tags, like "comment", here.
return array();
}
/**
* @task mail
*/
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->getStrongestAction($object, $xactions)->getActionName();
}
/**
* @task mail
*/
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTo(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
return array();
}
/**
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
$phids = array();
$has_support = false;
if ($object instanceof PhabricatorSubscribableInterface) {
$phid = $object->getPHID();
$phids[] = PhabricatorSubscribersQuery::loadSubscribersForPHID($phid);
$has_support = true;
}
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($project_phids)
->needWatchers(true)
->execute();
$watcher_phids = array();
foreach ($projects as $project) {
foreach ($project->getAllAncestorWatcherPHIDs() as $phid) {
$watcher_phids[$phid] = $phid;
}
}
if ($watcher_phids) {
// We need to do a visibility check for all the watchers, as
// watching a project is not a guarantee that you can see objects
// associated with it.
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireActor())
->withPHIDs($watcher_phids)
->execute();
$watchers = array();
foreach ($users as $user) {
$can_see = PhabricatorPolicyFilter::hasCapability(
$user,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($can_see) {
$watchers[] = $user->getPHID();
}
}
$phids[] = $watchers;
}
}
$has_support = true;
}
if (!$has_support) {
throw new Exception(
pht('The object being edited does not implement any standard '.
'interfaces (like PhabricatorSubscribableInterface) which allow '.
'CCs to be generated automatically. Override the "getMailCC()" '.
'method and generate CCs explicitly.'));
}
return array_mergev($phids);
}
/**
* @task mail
*/
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = id(new PhabricatorMetaMTAMailBody())
->setViewer($this->requireActor())
->setContextObject($object);
$this->addHeadersAndCommentsToMailBody($body, $xactions);
$this->addCustomFieldsToMailBody($body, $object, $xactions);
return $body;
}
/**
* @task mail
*/
protected function addEmailPreferenceSectionToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
$href = PhabricatorEnv::getProductionURI(
'/settings/panel/emailpreferences/');
$body->addLinkSection(pht('EMAIL PREFERENCES'), $href);
}
/**
* @task mail
*/
protected function addHeadersAndCommentsToMailBody(
PhabricatorMetaMTAMailBody $body,
array $xactions,
$object_label = null,
$object_href = null) {
+ // First, remove transactions which shouldn't be rendered in mail.
+ foreach ($xactions as $key => $xaction) {
+ if ($xaction->shouldHideForMail($xactions)) {
+ unset($xactions[$key]);
+ }
+ }
+
$headers = array();
$headers_html = array();
$comments = array();
$details = array();
+ $seen_comment = false;
foreach ($xactions as $xaction) {
- if ($xaction->shouldHideForMail($xactions)) {
- continue;
- }
- $header = $xaction->getTitleForMail();
- if ($header !== null) {
- $headers[] = $header;
- }
+ // Most mail has zero or one comments. In these cases, we render the
+ // "alice added a comment." transaction in the header, like a normal
+ // transaction.
- $header_html = $xaction->getTitleForHTMLMail();
- if ($header_html !== null) {
- $headers_html[] = $header_html;
- }
+ // Some mail, like Differential undraft mail or "!history" mail, may
+ // have two or more comments. In these cases, we'll put the first
+ // "alice added a comment." transaction in the header normally, but
+ // move the other transactions down so they provide context above the
+ // actual comment.
$comment = $xaction->getBodyForMail();
if ($comment !== null) {
- $comments[] = $comment;
+ $is_comment = true;
+ $comments[] = array(
+ 'xaction' => $xaction,
+ 'comment' => $comment,
+ 'initial' => !$seen_comment,
+ );
+ } else {
+ $is_comment = false;
+ }
+
+ if (!$is_comment || !$seen_comment) {
+ $header = $xaction->getTitleForMail();
+ if ($header !== null) {
+ $headers[] = $header;
+ }
+
+ $header_html = $xaction->getTitleForHTMLMail();
+ if ($header_html !== null) {
+ $headers_html[] = $header_html;
+ }
}
if ($xaction->hasChangeDetailsForMail()) {
$details[] = $xaction;
}
+
+ if ($is_comment) {
+ $seen_comment = true;
+ }
}
$headers_text = implode("\n", $headers);
$body->addRawPlaintextSection($headers_text);
$headers_html = phutil_implode_html(phutil_tag('br'), $headers_html);
$header_button = null;
if ($object_label !== null) {
$button_style = array(
'text-decoration: none;',
'padding: 4px 8px;',
'margin: 0 8px 8px;',
'float: right;',
'color: #464C5C;',
'font-weight: bold;',
'border-radius: 3px;',
'background-color: #F7F7F9;',
'background-image: linear-gradient(to bottom,#fff,#f1f0f1);',
'display: inline-block;',
'border: 1px solid rgba(71,87,120,.2);',
);
$header_button = phutil_tag(
'a',
array(
'style' => implode(' ', $button_style),
'href' => $object_href,
),
$object_label);
}
- $xactions_style = array(
- );
+ $xactions_style = array();
$header_action = phutil_tag(
'td',
array(),
$header_button);
$header_action = phutil_tag(
'td',
array(
'style' => implode(' ', $xactions_style),
),
array(
$headers_html,
// Add an extra newline to prevent the "View Object" button from
// running into the transaction text in Mail.app text snippet
// previews.
"\n",
));
$headers_html = phutil_tag(
'table',
array(),
phutil_tag('tr', array(), array($header_action, $header_button)));
$body->addRawHTMLSection($headers_html);
- foreach ($comments as $comment) {
+ foreach ($comments as $spec) {
+ $xaction = $spec['xaction'];
+ $comment = $spec['comment'];
+ $is_initial = $spec['initial'];
+
+ // If this is not the first comment in the mail, add the header showing
+ // who wrote the comment immediately above the comment.
+ if (!$is_initial) {
+ $header = $xaction->getTitleForMail();
+ if ($header !== null) {
+ $body->addRawPlaintextSection($header);
+ }
+
+ $header_html = $xaction->getTitleForHTMLMail();
+ if ($header_html !== null) {
+ $body->addRawHTMLSection($header_html);
+ }
+ }
+
$body->addRemarkupSection(null, $comment);
}
foreach ($details as $xaction) {
$details = $xaction->renderChangeDetailsForMail($body->getViewer());
if ($details !== null) {
$label = $this->getMailDiffSectionHeader($xaction);
$body->addHTMLSection($label, $details);
}
}
}
private function getMailDiffSectionHeader($xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getMailDiffSectionHeader();
}
return pht('EDIT DETAILS');
}
/**
* @task mail
*/
protected function addCustomFieldsToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
if ($object instanceof PhabricatorCustomFieldInterface) {
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
$field_list->setViewer($this->getActor());
$field_list->readFieldsFromStorage($object);
foreach ($field_list->getFields() as $field) {
$field->updateTransactionMailBody(
$body,
$this,
$xactions);
}
}
}
/**
* @task mail
*/
private function runHeraldMailRules(array $messages) {
foreach ($messages as $message) {
$engine = new HeraldEngine();
$adapter = id(new PhabricatorMailOutboundMailHeraldAdapter())
->setObject($message);
$rules = $engine->loadRulesForAdapter($adapter);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
}
}
/* -( Publishing Feed Stories )-------------------------------------------- */
/**
* @task feed
*/
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task feed
*/
protected function getFeedStoryType() {
return 'PhabricatorApplicationTransactionFeedStory';
}
/**
* @task feed
*/
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = array(
$object->getPHID(),
$this->getActingAsPHID(),
);
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
foreach ($project_phids as $project_phid) {
$phids[] = $project_phid;
}
}
return $phids;
}
/**
* @task feed
*/
protected function getFeedNotifyPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
return array_unique(array_merge(
$this->getMailTo($object),
$this->getMailCC($object)));
}
/**
* @task feed
*/
protected function getFeedStoryData(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = msort($xactions, 'getActionStrength');
$xactions = array_reverse($xactions);
return array(
'objectPHID' => $object->getPHID(),
'transactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
/**
* @task feed
*/
protected function publishFeedStory(
PhabricatorLiskDAO $object,
array $xactions,
array $mailed_phids) {
$xactions = mfilter($xactions, 'shouldHideForFeed', true);
if (!$xactions) {
return;
}
$related_phids = $this->feedRelatedPHIDs;
$subscribed_phids = $this->feedNotifyPHIDs;
// Remove muted users from the subscription list so they don't get
// notifications, either.
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$subscribed_phids = array_fuse($subscribed_phids);
foreach ($muted_phids as $muted_phid) {
unset($subscribed_phids[$muted_phid]);
}
$subscribed_phids = array_values($subscribed_phids);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
$unexpandable_phids = $this->mailUnexpandablePHIDs;
if (!is_array($unexpandable_phids)) {
$unexpandable_phids = array();
}
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($this->getActingAsPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->setUnexpandablePHIDs($unexpandable_phids)
->setMailRecipientPHIDs($mailed_phids)
->setMailTags($this->getMailTags($object, $xactions))
->publish();
}
/* -( Search Index )------------------------------------------------------- */
/**
* @task search
*/
protected function supportsSearch() {
return false;
}
/* -( Herald Integration )-------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
throw new Exception(pht('No herald adapter specified.'));
}
private function setHeraldAdapter(HeraldAdapter $adapter) {
$this->heraldAdapter = $adapter;
return $this;
}
protected function getHeraldAdapter() {
return $this->heraldAdapter;
}
private function setHeraldTranscript(HeraldTranscript $transcript) {
$this->heraldTranscript = $transcript;
return $this;
}
protected function getHeraldTranscript() {
return $this->heraldTranscript;
}
private function applyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
$adapter = $this->buildHeraldAdapter($object, $xactions)
->setContentSource($this->getContentSource())
->setIsNewObject($this->getIsNewObject())
->setActingAsPHID($this->getActingAsPHID())
->setAppliedTransactions($xactions);
if ($this->getApplicationEmail()) {
$adapter->setApplicationEmail($this->getApplicationEmail());
}
// If this editor is operating in silent mode, tell Herald that we aren't
// going to send any mail. This allows it to skip "the first time this
// rule matches, send me an email" rules which would otherwise match even
// though we aren't going to send any mail.
if ($this->getIsSilent()) {
$adapter->setForbiddenAction(
HeraldMailableState::STATECONST,
HeraldCoreStateReasons::REASON_SILENT);
}
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$this->setHeraldAdapter($adapter);
$this->setHeraldTranscript($xscript);
if ($adapter instanceof HarbormasterBuildableAdapterInterface) {
$buildable_phid = $adapter->getHarbormasterBuildablePHID();
HarbormasterBuildable::applyBuildPlans(
$buildable_phid,
$adapter->getHarbormasterContainerPHID(),
$adapter->getQueuedHarbormasterBuildRequests());
// Whether we queued any builds or not, any automatic buildable for this
// object is now done preparing builds and can transition into a
// completed status.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withManualBuildables(false)
->withBuildablePHIDs(array($buildable_phid))
->execute();
foreach ($buildables as $buildable) {
// If this buildable has already moved beyond preparation, we don't
// need to nudge it again.
if (!$buildable->isPreparing()) {
continue;
}
$buildable->sendMessage(
$this->getActor(),
HarbormasterMessageType::BUILDABLE_BUILD,
true);
}
}
$this->mustEncrypt = $adapter->getMustEncryptReasons();
return array_merge(
$this->didApplyHeraldRules($object, $adapter, $xscript),
$adapter->getQueuedTransactions());
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
return array();
}
/* -( Custom Fields )------------------------------------------------------ */
/**
* @task customfield
*/
private function getCustomFieldForTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$field_key = $xaction->getMetadataValue('customfield:key');
if (!$field_key) {
throw new Exception(
pht(
"Custom field transaction has no '%s'!",
'customfield:key'));
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$field_key);
if (!$field) {
throw new Exception(
pht(
"Custom field transaction has invalid '%s'; field '%s' ".
"is disabled or does not exist.",
'customfield:key',
$field_key));
}
if (!$field->shouldAppearInApplicationTransactions()) {
throw new Exception(
pht(
"Custom field transaction '%s' does not implement ".
"integration for %s.",
$field_key,
'ApplicationTransactions'));
}
$field->setViewer($this->getActor());
return $field;
}
/* -( Files )-------------------------------------------------------------- */
/**
* Extract the PHIDs of any files which these transactions attach.
*
* @task files
*/
private function extractFilePHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$changes = $this->getRemarkupChanges($xactions);
$blocks = mpull($changes, 'getNewValue');
$phids = array();
if ($blocks) {
$phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$this->getActor(),
$blocks);
}
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$phids[] = $xtype->extractFilePHIDs($object, $xaction->getNewValue());
} else {
$phids[] = $this->extractFilePHIDsFromCustomTransaction(
$object,
$xaction);
}
}
$phids = array_unique(array_filter(array_mergev($phids)));
if (!$phids) {
return array();
}
// Only let a user attach files they can actually see, since this would
// otherwise let you access any file by attaching it to an object you have
// view permission on.
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
return mpull($files, 'getPHID');
}
/**
* @task files
*/
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array();
}
/**
* @task files
*/
private function attachFiles(
PhabricatorLiskDAO $object,
array $file_phids) {
if (!$file_phids) {
return;
}
$editor = new PhabricatorEdgeEditor();
$src = $object->getPHID();
$type = PhabricatorObjectHasFileEdgeType::EDGECONST;
foreach ($file_phids as $dst) {
$editor->addEdge($src, $type, $dst);
}
$editor->save();
}
private function applyInverseEdgeTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction,
$inverse_type) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$add = array_fuse($add);
$rem = array_fuse($rem);
$all = $add + $rem;
$nodes = id(new PhabricatorObjectQuery())
->setViewer($this->requireActor())
->withPHIDs($all)
->execute();
foreach ($nodes as $node) {
if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
continue;
}
if ($node instanceof PhabricatorUser) {
// TODO: At least for now, don't record inverse edge transactions
// for users (for example, "alincoln joined project X"): Feed fills
// this role instead.
continue;
}
$editor = $node->getApplicationTransactionEditor();
$template = $node->getApplicationTransactionTemplate();
$target = $node->getApplicationTransactionObject();
if (isset($add[$node->getPHID()])) {
$edge_edit_type = '+';
} else {
$edge_edit_type = '-';
}
$template
->setTransactionType($xaction->getTransactionType())
->setMetadataValue('edge:type', $inverse_type)
->setNewValue(
array(
$edge_edit_type => array($object->getPHID() => $object->getPHID()),
));
$editor
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsInverseEdgeEditor(true)
+ ->setIsSilent($this->getIsSilent())
->setActor($this->requireActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
$editor->applyTransactions($target, array($template));
}
}
/* -( Workers )------------------------------------------------------------ */
/**
* Load any object state which is required to publish transactions.
*
* This hook is invoked in the main process before we compute data related
* to publishing transactions (like email "To" and "CC" lists), and again in
* the worker before publishing occurs.
*
* @return object Publishable object.
* @task workers
*/
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
return $object;
}
/**
* Convert the editor state to a serializable dictionary which can be passed
* to a worker.
*
* This data will be loaded with @{method:loadWorkerState} in the worker.
*
* @return dict<string, wild> Serializable editor state.
* @task workers
*/
final private function getWorkerState() {
$state = array();
foreach ($this->getAutomaticStateProperties() as $property) {
$state[$property] = $this->$property;
}
$custom_state = $this->getCustomWorkerState();
$custom_encoding = $this->getCustomWorkerStateEncoding();
$state += array(
'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(),
'custom' => $this->encodeStateForStorage($custom_state, $custom_encoding),
'custom.encoding' => $custom_encoding,
);
return $state;
}
/**
* Hook; return custom properties which need to be passed to workers.
*
* @return dict<string, wild> Custom properties.
* @task workers
*/
protected function getCustomWorkerState() {
return array();
}
/**
* Hook; return storage encoding for custom properties which need to be
* passed to workers.
*
* This primarily allows binary data to be passed to workers and survive
* JSON encoding.
*
* @return dict<string, string> Property encodings.
* @task workers
*/
protected function getCustomWorkerStateEncoding() {
return array();
}
/**
* Load editor state using a dictionary emitted by @{method:getWorkerState}.
*
* This method is used to load state when running worker operations.
*
* @param dict<string, wild> Editor state, from @{method:getWorkerState}.
* @return this
* @task workers
*/
final public function loadWorkerState(array $state) {
foreach ($this->getAutomaticStateProperties() as $property) {
$this->$property = idx($state, $property);
}
$exclude = idx($state, 'excludeMailRecipientPHIDs', array());
$this->setExcludeMailRecipientPHIDs($exclude);
$custom_state = idx($state, 'custom', array());
$custom_encodings = idx($state, 'custom.encoding', array());
$custom = $this->decodeStateFromStorage($custom_state, $custom_encodings);
$this->loadCustomWorkerState($custom);
return $this;
}
/**
* Hook; set custom properties on the editor from data emitted by
* @{method:getCustomWorkerState}.
*
* @param dict<string, wild> Custom state,
* from @{method:getCustomWorkerState}.
* @return this
* @task workers
*/
protected function loadCustomWorkerState(array $state) {
return $this;
}
/**
* Get a list of object properties which should be automatically sent to
* workers in the state data.
*
* These properties will be automatically stored and loaded by the editor in
* the worker.
*
* @return list<string> List of properties.
* @task workers
*/
private function getAutomaticStateProperties() {
return array(
'parentMessageID',
'isNewObject',
'heraldEmailPHIDs',
'heraldForcedEmailPHIDs',
'heraldHeader',
'mailToPHIDs',
'mailCCPHIDs',
'feedNotifyPHIDs',
'feedRelatedPHIDs',
'feedShouldPublish',
'mailShouldSend',
'mustEncrypt',
'mailStamps',
'mailUnexpandablePHIDs',
'mailMutedPHIDs',
'webhookMap',
'silent',
+ 'sendHistory',
);
}
/**
* Apply encodings prior to storage.
*
* See @{method:getCustomWorkerStateEncoding}.
*
* @param map<string, wild> Map of values to encode.
* @param map<string, string> Map of encodings to apply.
* @return map<string, wild> Map of encoded values.
* @task workers
*/
final private function encodeStateForStorage(
array $state,
array $encodings) {
foreach ($state as $key => $value) {
$encoding = idx($encodings, $key);
switch ($encoding) {
case self::STORAGE_ENCODING_BINARY:
// The mechanics of this encoding (serialize + base64) are a little
// awkward, but it allows us encode arrays and still be JSON-safe
// with binary data.
$value = @serialize($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to serialize() value for key "%s".',
$key));
}
$value = base64_encode($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to base64 encode value for key "%s".',
$key));
}
break;
}
$state[$key] = $value;
}
return $state;
}
/**
* Undo storage encoding applied when storing state.
*
* See @{method:getCustomWorkerStateEncoding}.
*
* @param map<string, wild> Map of encoded values.
* @param map<string, string> Map of encodings.
* @return map<string, wild> Map of decoded values.
* @task workers
*/
final private function decodeStateFromStorage(
array $state,
array $encodings) {
foreach ($state as $key => $value) {
$encoding = idx($encodings, $key);
switch ($encoding) {
case self::STORAGE_ENCODING_BINARY:
$value = base64_decode($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to base64_decode() value for key "%s".',
$key));
}
$value = unserialize($value);
break;
}
$state[$key] = $value;
}
return $state;
}
/**
* Remove conflicts from a list of projects.
*
* Objects aren't allowed to be tagged with multiple milestones in the same
* group, nor projects such that one tag is the ancestor of any other tag.
* If the list of PHIDs include mutually exclusive projects, remove the
* conflicting projects.
*
* @param list<phid> List of project PHIDs.
* @return list<phid> List with conflicts removed.
*/
private function applyProjectConflictRules(array $phids) {
if (!$phids) {
return array();
}
// Overall, the last project in the list wins in cases of conflict (so when
// you add something, the thing you just added sticks and removes older
// values).
// Beyond that, there are two basic cases:
// Milestones: An object can't be in "A > Sprint 3" and "A > Sprint 4".
// If multiple projects are milestones of the same parent, we only keep the
// last one.
// Ancestor: You can't be in "A" and "A > B". If "A > B" comes later
// in the list, we remove "A" and keep "A > B". If "A" comes later, we
// remove "A > B" and keep "A".
// Note that it's OK to be in "A > B" and "A > C". There's only a conflict
// if one project is an ancestor of another. It's OK to have something
// tagged with multiple projects which share a common ancestor, so long as
// they are not mutual ancestors.
$viewer = PhabricatorUser::getOmnipotentUser();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array_keys($phids))
->execute();
$projects = mpull($projects, null, 'getPHID');
// We're going to build a map from each project with milestones to the last
// milestone in the list. This last milestone is the milestone we'll keep.
$milestone_map = array();
// We're going to build a set of the projects which have no descendants
// later in the list. This allows us to apply both ancestor rules.
$ancestor_map = array();
foreach ($phids as $phid => $ignored) {
$project = idx($projects, $phid);
if (!$project) {
continue;
}
// This is the last milestone we've seen, so set it as the selection for
// the project's parent. This might be setting a new value or overwriting
// an earlier value.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$milestone_map[$parent_phid] = $phid;
}
// Since this is the last item in the list we've examined so far, add it
// to the set of projects with no later descendants.
$ancestor_map[$phid] = $phid;
// Remove any ancestors from the set, since this is a later descendant.
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
unset($ancestor_map[$ancestor_phid]);
}
}
// Now that we've built the maps, we can throw away all the projects which
// have conflicts.
foreach ($phids as $phid => $ignored) {
$project = idx($projects, $phid);
if (!$project) {
// If a PHID is invalid, we just leave it as-is. We could clean it up,
// but leaving it untouched is less likely to cause collateral damage.
continue;
}
// If this was a milestone, check if it was the last milestone from its
// group in the list. If not, remove it from the list.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
if ($milestone_map[$parent_phid] !== $phid) {
unset($phids[$phid]);
continue;
}
}
// If a later project in the list is a subproject of this one, it will
// have removed ancestors from the map. If this project does not point
// at itself in the ancestor map, it should be discarded in favor of a
// subproject that comes later.
if (idx($ancestor_map, $phid) !== $phid) {
unset($phids[$phid]);
continue;
}
// If a later project in the list is an ancestor of this one, it will
// have added itself to the map. If any ancestor of this project points
// at itself in the map, this project should be discarded in favor of
// that later ancestor.
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
if (isset($ancestor_map[$ancestor_phid])) {
unset($phids[$phid]);
continue 2;
}
}
}
return $phids;
}
/**
* When the view policy for an object is changed, scramble the secret keys
* for attached files to invalidate existing URIs.
*/
private function scrambleFileSecrets($object) {
// If this is a newly created object, we don't need to scramble anything
// since it couldn't have been previously published.
if ($this->getIsNewObject()) {
return;
}
// If the object is a file itself, scramble it.
if ($object instanceof PhabricatorFile) {
if ($this->shouldScramblePolicy($object->getViewPolicy())) {
$object->scrambleSecret();
$object->save();
}
}
$phid = $object->getPHID();
$attached_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
PhabricatorObjectHasFileEdgeType::EDGECONST);
if (!$attached_phids) {
return;
}
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$files = id(new PhabricatorFileQuery())
->setViewer($omnipotent_viewer)
->withPHIDs($attached_phids)
->execute();
foreach ($files as $file) {
$view_policy = $file->getViewPolicy();
if ($this->shouldScramblePolicy($view_policy)) {
$file->scrambleSecret();
$file->save();
}
}
}
/**
* Check if a policy is strong enough to justify scrambling. Objects which
* are set to very open policies don't need to scramble their files, and
* files with very open policies don't need to be scrambled when associated
* objects change.
*/
private function shouldScramblePolicy($policy) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
case PhabricatorPolicies::POLICY_USER:
return false;
}
return true;
}
private function updateWorkboardColumns($object, $const, $old, $new) {
// If an object is removed from a project, remove it from any proxy
// columns for that project. This allows a task which is moved up from a
// milestone to the parent to move back into the "Backlog" column on the
// parent workboard.
if ($const != PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) {
return;
}
// TODO: This should likely be some future WorkboardInterface.
$appears_on_workboards = ($object instanceof ManiphestTask);
if (!$appears_on_workboards) {
return;
}
$removed_phids = array_keys(array_diff_key($old, $new));
if (!$removed_phids) {
return;
}
// Find any proxy columns for the removed projects.
$proxy_columns = id(new PhabricatorProjectColumnQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withProxyPHIDs($removed_phids)
->execute();
if (!$proxy_columns) {
return array();
}
$proxy_phids = mpull($proxy_columns, 'getPHID');
$position_table = new PhabricatorProjectColumnPosition();
$conn_w = $position_table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND columnPHID IN (%Ls)',
$position_table->getTableName(),
$object->getPHID(),
$proxy_phids);
}
private function getModularTransactionTypes() {
if ($this->modularTypes === null) {
$template = $this->object->getApplicationTransactionTemplate();
if ($template instanceof PhabricatorModularTransaction) {
$xtypes = $template->newModularTransactionTypes();
foreach ($xtypes as $key => $xtype) {
$xtype = clone $xtype;
$xtype->setEditor($this);
$xtypes[$key] = $xtype;
}
} else {
$xtypes = array();
}
$this->modularTypes = $xtypes;
}
return $this->modularTypes;
}
private function getModularTransactionType($type) {
$types = $this->getModularTransactionTypes();
return idx($types, $type);
}
private function willApplyTransactions($object, array $xactions) {
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if (!$xtype) {
continue;
}
$xtype->willApplyTransactions($object, $xactions);
}
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this object.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created an object: %s.', $author, $object);
}
/* -( Queue )-------------------------------------------------------------- */
protected function queueTransaction(
PhabricatorApplicationTransaction $xaction) {
$this->transactionQueue[] = $xaction;
return $this;
}
private function flushTransactionQueue($object) {
if (!$this->transactionQueue) {
return;
}
$xactions = $this->transactionQueue;
$this->transactionQueue = array();
$editor = $this->newQueueEditor();
return $editor->applyTransactions($object, $xactions);
}
private function newQueueEditor() {
$editor = id(newv(get_class($this), array()))
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setContinueOnMissingFields($this->getContinueOnMissingFields())
->setIsSilent($this->getIsSilent());
if ($this->actingAsPHID !== null) {
$editor->setActingAsPHID($this->actingAsPHID);
}
return $editor;
}
/* -( Stamps )------------------------------------------------------------- */
public function newMailStampTemplates($object) {
$actor = $this->getActor();
$templates = array();
$extensions = $this->newMailExtensions($object);
foreach ($extensions as $extension) {
$stamps = $extension->newMailStampTemplates($object);
foreach ($stamps as $stamp) {
$key = $stamp->getKey();
if (isset($templates[$key])) {
throw new Exception(
pht(
'Mail extension ("%s") defines a stamp template with the '.
'same key ("%s") as another template. Each stamp template '.
'must have a unique key.',
get_class($extension),
$key));
}
$stamp->setViewer($actor);
$templates[$key] = $stamp;
}
}
return $templates;
}
final public function getMailStamp($key) {
if (!isset($this->stampTemplates)) {
throw new PhutilInvalidStateException('newMailStampTemplates');
}
if (!isset($this->stampTemplates[$key])) {
throw new Exception(
pht(
'Editor ("%s") has no mail stamp template with provided key ("%s").',
get_class($this),
$key));
}
return $this->stampTemplates[$key];
}
private function newMailStamps($object, array $xactions) {
$actor = $this->getActor();
$this->stampTemplates = $this->newMailStampTemplates($object);
$extensions = $this->newMailExtensions($object);
$stamps = array();
foreach ($extensions as $extension) {
$extension->newMailStamps($object, $xactions);
}
return $this->stampTemplates;
}
private function newMailExtensions($object) {
$actor = $this->getActor();
$all_extensions = PhabricatorMailEngineExtension::getAllExtensions();
$extensions = array();
foreach ($all_extensions as $key => $template) {
$extension = id(clone $template)
->setViewer($actor)
->setEditor($this);
if ($extension->supportsObject($object)) {
$extensions[$key] = $extension;
}
}
return $extensions;
}
private function generateMailStamps($object, $data) {
if (!$data || !is_array($data)) {
return null;
}
$templates = $this->newMailStampTemplates($object);
foreach ($data as $spec) {
if (!is_array($spec)) {
continue;
}
$key = idx($spec, 'key');
if (!isset($templates[$key])) {
continue;
}
$type = idx($spec, 'type');
if ($templates[$key]->getStampType() !== $type) {
continue;
}
$value = idx($spec, 'value');
$templates[$key]->setValueFromDictionary($value);
}
$results = array();
foreach ($templates as $template) {
$value = $template->getValueForRendering();
$rendered = $template->renderStamps($value);
if ($rendered === null) {
continue;
}
$rendered = (array)$rendered;
foreach ($rendered as $stamp) {
$results[] = $stamp;
}
}
natcasesort($results);
return $results;
}
public function getRemovedRecipientPHIDs() {
return $this->mailRemovedPHIDs;
}
private function buildOldRecipientLists($object, $xactions) {
// See T4776. Before we start making any changes, build a list of the old
// recipients. If a change removes a user from the recipient list for an
// object we still want to notify the user about that change. This allows
// them to respond if they didn't want to be removed.
if (!$this->shouldSendMail($object, $xactions)) {
return;
}
$this->oldTo = $this->getMailTo($object);
$this->oldCC = $this->getMailCC($object);
return $this;
}
private function applyOldRecipientLists() {
$actor_phid = $this->getActingAsPHID();
// If you took yourself off the recipient list (for example, by
// unsubscribing or resigning) assume that you know what you did and
// don't need to be notified.
// If you just moved from "To" to "Cc" (or vice versa), you're still a
// recipient so we don't need to add you back in.
$map = array_fuse($this->mailToPHIDs) + array_fuse($this->mailCCPHIDs);
foreach ($this->oldTo as $phid) {
if ($phid === $actor_phid) {
continue;
}
if (isset($map[$phid])) {
continue;
}
$this->mailToPHIDs[] = $phid;
$this->mailRemovedPHIDs[] = $phid;
}
foreach ($this->oldCC as $phid) {
if ($phid === $actor_phid) {
continue;
}
if (isset($map[$phid])) {
continue;
}
$this->mailCCPHIDs[] = $phid;
$this->mailRemovedPHIDs[] = $phid;
}
return $this;
}
private function queueWebhooks($object, array $xactions) {
$hook_viewer = PhabricatorUser::getOmnipotentUser();
$webhook_map = $this->webhookMap;
if (!is_array($webhook_map)) {
$webhook_map = array();
}
// Add any "Firehose" hooks to the list of hooks we're going to call.
$firehose_hooks = id(new HeraldWebhookQuery())
->setViewer($hook_viewer)
->withStatuses(
array(
HeraldWebhook::HOOKSTATUS_FIREHOSE,
))
->execute();
foreach ($firehose_hooks as $firehose_hook) {
// This is "the hook itself is the reason this hook is being called",
// since we're including it because it's configured as a firehose
// hook.
$hook_phid = $firehose_hook->getPHID();
$webhook_map[$hook_phid][] = $hook_phid;
}
if (!$webhook_map) {
return;
}
// NOTE: We're going to queue calls to disabled webhooks, they'll just
// immediately fail in the worker queue. This makes the behavior more
// visible.
$call_hooks = id(new HeraldWebhookQuery())
->setViewer($hook_viewer)
->withPHIDs(array_keys($webhook_map))
->execute();
foreach ($call_hooks as $call_hook) {
$trigger_phids = idx($webhook_map, $call_hook->getPHID());
$request = HeraldWebhookRequest::initializeNewWebhookRequest($call_hook)
->setObjectPHID($object->getPHID())
->setTransactionPHIDs(mpull($xactions, 'getPHID'))
->setTriggerPHIDs($trigger_phids)
->setRetryMode(HeraldWebhookRequest::RETRY_FOREVER)
->setIsSilentAction((bool)$this->getIsSilent())
->setIsSecureAction((bool)$this->getMustEncrypt())
->save();
$request->queueCall();
}
}
private function hasWarnings($object, $xaction) {
// TODO: For the moment, this is a very un-modular hack to support
// exactly one type of warning (mentioning users on a draft revision)
// that we want to show. See PHI433.
if (!($object instanceof DifferentialRevision)) {
return false;
}
if (!$object->isDraft()) {
return false;
}
$type = $xaction->getTransactionType();
if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
return false;
}
// NOTE: This will currently warn even if you're only removing
// subscribers.
return true;
}
+ private function buildHistoryMail(PhabricatorLiskDAO $object) {
+ $viewer = $this->requireActor();
+ $recipient_phid = $this->getActingAsPHID();
+
+ // Load every transaction so we can build a mail message with a complete
+ // history for the object.
+ $query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
+ $xactions = $query
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($object->getPHID()))
+ ->execute();
+ $xactions = array_reverse($xactions);
+
+ $mail_messages = $this->buildMailWithRecipients(
+ $object,
+ $xactions,
+ array($recipient_phid),
+ array(),
+ array());
+ $mail = head($mail_messages);
+
+ // Since the user explicitly requested "!history", force delivery of this
+ // message regardless of their other mail settings.
+ $mail->setForceDelivery(true);
+
+ return $mail;
+ }
+
}
diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
index 260c0d2ac..338702478 100644
--- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
+++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php
@@ -1,55 +1,60 @@
<?php
final class PhabricatorSubtypeEditEngineExtension
extends PhabricatorEditEngineExtension {
const EXTENSIONKEY = 'editengine.subtype';
const EDITKEY = 'subtype';
public function getExtensionPriority() {
return 8000;
}
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('Subtypes');
}
public function supportsObject(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
return $engine->supportsSubtypes();
}
public function buildCustomEditFields(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
$subtype_type = PhabricatorTransactions::TYPE_SUBTYPE;
$map = $object->newEditEngineSubtypeMap();
$options = mpull($map, 'getName');
$subtype_field = id(new PhabricatorSelectEditField())
->setKey(self::EDITKEY)
->setLabel(pht('Subtype'))
->setIsConduitOnly(true)
->setIsHidden(true)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setTransactionType($subtype_type)
->setConduitDescription(pht('Change the object subtype.'))
->setConduitTypeDescription(pht('New object subtype key.'))
->setValue($object->getEditEngineSubtype())
->setOptions($options);
+ // If subtypes are configured, enable changing them from the bulk editor.
+ if (count($map) > 1) {
+ $subtype_field->setBulkEditLabel(pht('Change subtype to'));
+ }
+
return array(
$subtype_field,
);
}
}
diff --git a/src/applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php
index c9f43f49b..4eeda3251 100644
--- a/src/applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php
+++ b/src/applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php
@@ -1,45 +1,61 @@
<?php
final class PhabricatorTransactionsFulltextEngineExtension
extends PhabricatorFulltextEngineExtension {
const EXTENSIONKEY = 'transactions';
public function getExtensionName() {
return pht('Comments');
}
public function shouldEnrichFulltextObject($object) {
return ($object instanceof PhabricatorApplicationTransactionInterface);
}
public function enrichFulltextObject(
$object,
PhabricatorSearchAbstractDocument $document) {
$query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
if (!$query) {
return;
}
- $xactions = $query
+ $query
->setViewer($this->getViewer())
->withObjectPHIDs(array($object->getPHID()))
->withComments(true)
- ->needComments(true)
- ->execute();
+ ->needComments(true);
+
+ // See PHI719. Users occasionally create objects with huge numbers of
+ // comments, which can be slow to index. We handle this with reasonable
+ // grace: at time of writing, we can index a task with 100K comments in
+ // about 30 seconds. However, we do need to hold all the comments in
+ // memory in the AbstractDocument, so there's some practical limit to what
+ // we can realistically index.
+
+ // Since objects with more than 1,000 comments are not likely to be
+ // legitimate objects with actual discussion, index only the first
+ // thousand comments.
+
+ $query
+ ->setOrderVector(array('-id'))
+ ->setLimit(1000);
+
+ $xactions = $query->execute();
foreach ($xactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$comment = $xaction->getComment();
$document->addField(
PhabricatorSearchDocumentFieldType::FIELD_COMMENT,
$comment->getContent());
}
}
}
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 69d11fe75..ed88ac98d 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1628 +1,1635 @@
<?php
abstract class PhabricatorApplicationTransaction
extends PhabricatorLiskDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const TARGET_TEXT = 'text';
const TARGET_HTML = 'html';
protected $phid;
protected $objectPHID;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $commentPHID;
protected $commentVersion = 0;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $metadata = array();
protected $contentSource;
private $comment;
private $commentNotLoaded;
private $handles;
private $renderingTarget = self::TARGET_HTML;
private $transactionGroup = array();
private $viewer = self::ATTACHABLE;
private $object = self::ATTACHABLE;
private $oldValueHasBeenSet = false;
private $ignoreOnNoEffect;
/**
* Flag this transaction as a pure side-effect which should be ignored when
* applying transactions if it has no effect, even if transaction application
* would normally fail. This both provides users with better error messages
* and allows transactions to perform optional side effects.
*/
public function setIgnoreOnNoEffect($ignore) {
$this->ignoreOnNoEffect = $ignore;
return $this;
}
public function getIgnoreOnNoEffect() {
return $this->ignoreOnNoEffect;
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
case PhabricatorTransactions::TYPE_INLINESTATE:
return false;
}
return true;
}
abstract public function getApplicationTransactionType();
private function getApplicationObjectTypeName() {
$types = PhabricatorPHIDType::getAllTypes();
$type = idx($types, $this->getApplicationTransactionType());
if ($type) {
return $type->getTypeName();
}
return pht('Object');
}
public function getApplicationTransactionCommentObject() {
throw new PhutilMethodNotImplementedException();
}
public function getApplicationTransactionViewObject() {
return new PhabricatorApplicationTransactionView();
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function generatePHID() {
$type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
$subtype = $this->getApplicationTransactionType();
return PhabricatorPHID::generateNewPHID($type, $subtype);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'commentPHID' => 'phid?',
'commentVersion' => 'uint32',
'contentSource' => 'text',
'transactionType' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function hasComment() {
return $this->getComment() && strlen($this->getComment()->getContent());
}
public function getComment() {
if ($this->commentNotLoaded) {
throw new Exception(pht('Comment for this transaction was not loaded.'));
}
return $this->comment;
}
public function setIsCreateTransaction($create) {
return $this->setMetadataValue('core.create', $create);
}
public function getIsCreateTransaction() {
return (bool)$this->getMetadataValue('core.create', false);
}
public function setIsDefaultTransaction($default) {
return $this->setMetadataValue('core.default', $default);
}
public function getIsDefaultTransaction() {
return (bool)$this->getMetadataValue('core.default', false);
}
public function setIsSilentTransaction($silent) {
return $this->setMetadataValue('core.silent', $silent);
}
public function getIsSilentTransaction() {
return (bool)$this->getMetadataValue('core.silent', false);
}
public function attachComment(
PhabricatorApplicationTransactionComment $comment) {
$this->comment = $comment;
$this->commentNotLoaded = false;
return $this;
}
public function setCommentNotLoaded($not_loaded) {
$this->commentNotLoaded = $not_loaded;
return $this;
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function getRemarkupChanges() {
$changes = $this->newRemarkupChanges();
assert_instances_of($changes, 'PhabricatorTransactionRemarkupChange');
// Convert older-style remarkup blocks into newer-style remarkup changes.
// This builds changes that do not have the correct "old value", so rules
// that operate differently against edits (like @user mentions) won't work
// properly.
foreach ($this->getRemarkupBlocks() as $block) {
$changes[] = $this->newRemarkupChange()
->setOldValue(null)
->setNewValue($block);
}
$comment = $this->getComment();
if ($comment) {
if ($comment->hasOldComment()) {
$old_value = $comment->getOldComment()->getContent();
} else {
$old_value = null;
}
$new_value = $comment->getContent();
$changes[] = $this->newRemarkupChange()
->setOldValue($old_value)
->setNewValue($new_value);
}
return $changes;
}
protected function newRemarkupChanges() {
return array();
}
protected function newRemarkupChange() {
return id(new PhabricatorTransactionRemarkupChange())
->setTransaction($this);
}
/**
* @deprecated
*/
public function getRemarkupBlocks() {
$blocks = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
$this);
foreach ($custom_blocks as $custom_block) {
$blocks[] = $custom_block;
}
}
break;
}
return $blocks;
}
public function setOldValue($value) {
$this->oldValueHasBeenSet = true;
$this->writeField('oldValue', $value);
return $this;
}
public function hasOldValue() {
return $this->oldValueHasBeenSet;
}
public function newChronologicalSortVector() {
return id(new PhutilSortVector())
->addInt((int)$this->getDateCreated())
->addInt((int)$this->getID());
}
/* -( Rendering )---------------------------------------------------------- */
public function setRenderingTarget($rendering_target) {
$this->renderingTarget = $rendering_target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
public function attachViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->assertAttached($this->viewer);
}
public function getRequiredHandlePHIDs() {
$phids = array();
$old = $this->getOldValue();
$new = $this->getNewValue();
$phids[] = array($this->getAuthorPHID());
$phids[] = array($this->getObjectPHID());
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
$this);
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$phids[] = $old;
$phids[] = $new;
break;
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$phids[] = $record->getChangedPHIDs();
break;
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($new as $move) {
$phids[] = array(
$move['columnPHID'],
$move['boardPHID'],
);
$phids[] = $move['fromColumnPHIDs'];
}
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) {
$phids[] = array($old);
}
if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_SPACE:
if ($old) {
$phids[] = array($old);
}
if ($new) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_TOKEN:
break;
}
if ($this->getComment()) {
$phids[] = array($this->getComment()->getAuthorPHID());
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
pht(
'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
'did not load.',
$this->getPHID(),
$this->getTransactionType(),
$phid));
}
return $this->handles[$phid];
}
public function getHandleIfExists($phid) {
return idx($this->handles, $phid);
}
public function getHandles() {
if ($this->handles === null) {
throw new Exception(
pht('Transaction requires handles and it did not load them.'));
}
return $this->handles;
}
public function renderHandleLink($phid) {
if ($this->renderingTarget == self::TARGET_HTML) {
return $this->getHandle($phid)->renderLink();
} else {
return $this->getHandle($phid)->getLinkName();
}
}
public function renderHandleList(array $phids) {
$links = array();
foreach ($phids as $phid) {
$links[] = $this->renderHandleLink($phid);
}
if ($this->renderingTarget == self::TARGET_HTML) {
return phutil_implode_html(', ', $links);
} else {
return implode(', ', $links);
}
}
private function renderSubscriberList(array $phids, $change_type) {
if ($this->getRenderingTarget() == self::TARGET_TEXT) {
return $this->renderHandleList($phids);
} else {
$handles = array_select_keys($this->getHandles(), $phids);
return id(new SubscriptionListStringBuilder())
->setHandles($handles)
->setObjectPHID($this->getPHID())
->buildTransactionString($change_type);
}
}
protected function renderPolicyName($phid, $state = 'old') {
$policy = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$this->getHandleIfExists($phid));
if ($this->renderingTarget == self::TARGET_HTML) {
switch ($policy->getType()) {
case PhabricatorPolicyType::TYPE_CUSTOM:
$policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
$policy->setWorkflow(true);
break;
default:
break;
}
$output = $policy->renderDescription();
} else {
$output = hsprintf('%s', $policy->getFullName());
}
return $output;
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'fa-trash';
}
return 'fa-comment';
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return 'fa-user';
} else if ($add) {
return 'fa-user-plus';
} else if ($rem) {
return 'fa-user-times';
} else {
return 'fa-user';
}
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return 'fa-lock';
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
return 'fa-undo';
case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
return 'fa-ambulance';
}
return 'fa-link';
case PhabricatorTransactions::TYPE_TOKEN:
return 'fa-trophy';
case PhabricatorTransactions::TYPE_SPACE:
return 'fa-th-large';
case PhabricatorTransactions::TYPE_COLUMNS:
return 'fa-columns';
}
return 'fa-pencil';
}
public function getToken() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($new) {
$icon = substr($new, 10);
} else {
$icon = substr($old, 10);
}
return array($icon, !$this->getNewValue());
}
return array(null, null);
}
public function getColor() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT;
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'black';
}
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
return 'pink';
case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
return 'sky';
}
break;
}
return null;
}
protected function getTransactionCustomField() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$key = $this->getMetadataValue('customfield:key');
if (!$key) {
return null;
}
$object = $this->getObject();
if (!($object instanceof PhabricatorCustomFieldInterface)) {
return null;
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$key);
if (!$field) {
return null;
}
$field->setViewer($this->getViewer());
return $field;
}
return null;
}
public function shouldHide() {
// Never hide comments.
if ($this->hasComment()) {
return false;
}
+ $xaction_type = $this->getTransactionType();
+
+ // Always hide requests for object history.
+ if ($xaction_type === PhabricatorTransactions::TYPE_HISTORY) {
+ return true;
+ }
+
// Hide creation transactions if the old value is empty. These are
- // transactions like "alice set the task tile to: ...", which are
+ // transactions like "alice set the task title to: ...", which are
// essentially never interesting.
if ($this->getIsCreateTransaction()) {
- switch ($this->getTransactionType()) {
+ switch ($xaction_type) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
return true;
default:
$old = $this->getOldValue();
if (is_array($old) && !$old) {
return true;
}
if (!is_array($old)) {
if (!strlen($old)) {
return true;
}
// The integer 0 is also uninteresting by default; this is often
// an "off" flag for something like "All Day Event".
if ($old === 0) {
return true;
}
}
break;
}
}
// Hide creation transactions setting values to defaults, even if
// the old value is not empty. For example, tasks may have a global
// default view policy of "All Users", but a particular form sets the
// policy to "Administrators". The transaction corresponding to this
// change is not interesting, since it is the default behavior of the
// form.
if ($this->getIsCreateTransaction()) {
if ($this->getIsDefaultTransaction()) {
return true;
}
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
break;
}
// TODO: Remove this eventually, this is handling old changes during
// object creation prior to the introduction of "create" and "default"
// transaction display flags.
// NOTE: We can also hit this case with Space transactions that later
// update a default space (`null`) to an explicit space, so handling
// the Space case may require some finesse.
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->shouldHideInApplicationTransactions($this);
}
break;
case PhabricatorTransactions::TYPE_COLUMNS:
return !$this->getInterestingMoves($this->getNewValue());
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
case PhabricatorMutedEdgeType::EDGECONST:
case PhabricatorMutedByEdgeType::EDGECONST:
return true;
break;
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$add_value = reset($add);
$add_handle = $this->getHandle($add_value);
if ($add_handle->getPolicyFiltered()) {
return true;
}
return false;
break;
default:
break;
}
break;
}
return false;
}
public function shouldHideForMail(array $xactions) {
if ($this->isSelfSubscription()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
// When an object is first created, we hide any corresponding
// project transactions in the web UI because you can just look at
// the UI element elsewhere on screen to see which projects it
// is tagged with. However, in mail there's no other way to get
// this information, and it has some amount of value to users, so
// we keep the transaction. See T10493.
return false;
default:
break;
}
break;
}
if ($this->isInlineCommentTransaction()) {
$inlines = array();
// If there's a normal comment, we don't need to publish the inline
// transaction, since the normal comment covers things.
foreach ($xactions as $xaction) {
if ($xaction->isInlineCommentTransaction()) {
$inlines[] = $xaction;
continue;
}
// We found a normal comment, so hide this inline transaction.
if ($xaction->hasComment()) {
return true;
}
}
// If there are several inline comments, only publish the first one.
if ($this !== head($inlines)) {
return true;
}
}
return $this->shouldHide();
}
public function shouldHideForFeed() {
if ($this->isSelfSubscription()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
return true;
}
return $this->shouldHide();
}
public function getTitleForMail() {
return id(clone $this)->setRenderingTarget('text')->getTitle();
}
public function getTitleForHTMLMail() {
$title = $this->getTitleForMail();
if ($title === null) {
return null;
}
if ($this->hasChangeDetails()) {
$details_uri = $this->getChangeDetailsURI();
$details_uri = PhabricatorEnv::getProductionURI($details_uri);
$show_details = phutil_tag(
'a',
array(
'href' => $details_uri,
),
pht('(Show Details)'));
$title = array($title, ' ', $show_details);
}
return $title;
}
public function getChangeDetailsURI() {
return '/transactions/detail/'.$this->getPHID().'/';
}
public function getBodyForMail() {
if ($this->isInlineCommentTransaction()) {
// We don't return inline comment content as mail body content, because
// applications need to contextualize it (by adding line numbers, for
// example) in order for it to make sense.
return null;
}
$comment = $this->getComment();
if ($comment && strlen($comment->getContent())) {
return $comment->getContent();
}
return null;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('You can not post an empty comment.');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'This %s already has that view policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'This %s already has that edit policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'This %s already has that join policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'All users are already subscribed to this %s.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SPACE:
return pht('This object is already in that space.');
case PhabricatorTransactions::TYPE_EDGE:
return pht('Edges already exist; transaction has no effect.');
case PhabricatorTransactions::TYPE_COLUMNS:
return pht(
'You have not moved this object to any columns it is not '.
'already in.');
}
return pht(
'Transaction (of type "%s") has no effect.',
$this->getTransactionType());
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this object.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with visibility "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the visibility from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with edit policy "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the edit policy from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with join policy "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the join policy from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object in space %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted this object from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s edited subscriber(s), added %d: %s; removed %d: %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else if ($add) {
return pht(
'%s added %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'));
} else if ($rem) {
return pht(
'%s removed %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else {
// This is used when rendering previews, before the user actually
// selects any CCs.
return pht(
'%s updated subscribers...',
$this->renderHandleLink($author_phid));
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$rem = $record->getRemovedPHIDs();
$type = $this->getMetadata('edge:type');
$type = head($type);
try {
$type_obj = PhabricatorEdgeType::getByConstant($type);
} catch (Exception $ex) {
// Recover somewhat gracefully from edge transactions which
// we don't have the classes for.
return pht(
'%s edited an edge.',
$this->renderHandleLink($author_phid));
}
if ($add && $rem) {
return $type_obj->getTransactionEditString(
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add) + count($rem)),
phutil_count($add),
$this->renderHandleList($add),
phutil_count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getTransactionAddString(
$this->renderHandleLink($author_phid),
phutil_count($add),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getTransactionRemoveString(
$this->renderHandleLink($author_phid),
phutil_count($rem),
$this->renderHandleList($rem));
} else {
return $type_obj->getTransactionPreviewString(
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitle($this);
} else {
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited a custom field (with key "%s").',
$this->renderHandleLink($author_phid),
$this->getMetadata('customfield:key'));
} else {
return pht(
'%s edited a custom field.',
$this->renderHandleLink($author_phid));
}
}
case PhabricatorTransactions::TYPE_TOKEN:
if ($old && $new) {
return pht(
'%s updated a token.',
$this->renderHandleLink($author_phid));
} else if ($old) {
return pht(
'%s rescinded a token.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s awarded a token.',
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_INLINESTATE:
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
$done++;
} else {
$undone++;
}
}
if ($done && $undone) {
return pht(
'%s marked %s inline comment(s) as done and %s inline comment(s) '.
'as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done),
new PhutilNumber($undone));
} else if ($done) {
return pht(
'%s marked %s inline comment(s) as done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done));
} else {
return pht(
'%s marked %s inline comment(s) as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($undone));
}
break;
case PhabricatorTransactions::TYPE_COLUMNS:
$moves = $this->getInterestingMoves($new);
if (count($moves) == 1) {
$move = head($moves);
$from_columns = $move['fromColumnPHIDs'];
$to_column = $move['columnPHID'];
$board_phid = $move['boardPHID'];
if (count($from_columns) == 1) {
return pht(
'%s moved this task from %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(head($from_columns)),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
} else {
return pht(
'%s moved this task to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
}
} else {
$fragments = array();
foreach ($moves as $move) {
$fragments[] = pht(
'%s (%s)',
$this->renderHandleLink($board_phid),
$this->renderHandleLink($to_column));
}
return pht(
'%s moved this task on %s board(s): %s.',
$this->renderHandleLink($author_phid),
phutil_count($moves),
phutil_implode_html(', ', $fragments));
}
break;
default:
// In developer mode, provide a better hint here about which string
// we're missing.
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited this object (transaction type "%s").',
$this->renderHandleLink($author_phid),
$this->getTransactionType());
} else {
return pht(
'%s edited this %s.',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName());
}
}
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'%s changed the join policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'%s updated subscribers of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created %s in the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted %s from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$rem = $record->getRemovedPHIDs();
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_obj = PhabricatorEdgeType::getByConstant($type);
if ($add && $rem) {
return $type_obj->getFeedEditString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
new PhutilNumber(count($add) + count($rem)),
phutil_count($add),
$this->renderHandleList($add),
phutil_count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getFeedAddString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($add),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getFeedRemoveString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($rem),
$this->renderHandleList($rem));
} else {
return pht(
'%s edited edge metadata for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitleForFeed($this);
} else {
return pht(
'%s edited a custom field on %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_COLUMNS:
$moves = $this->getInterestingMoves($new);
if (count($moves) == 1) {
$move = head($moves);
$from_columns = $move['fromColumnPHIDs'];
$to_column = $move['columnPHID'];
$board_phid = $move['boardPHID'];
if (count($from_columns) == 1) {
return pht(
'%s moved %s from %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink(head($from_columns)),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
} else {
return pht(
'%s moved %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
}
} else {
$fragments = array();
foreach ($moves as $move) {
$fragments[] = pht(
'%s (%s)',
$this->renderHandleLink($board_phid),
$this->renderHandleLink($to_column));
}
return pht(
'%s moved %s on %s board(s): %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($moves),
phutil_implode_html(', ', $fragments));
}
break;
}
return $this->getTitle();
}
public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
$fields = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$fields[] = 'comment/'.$this->getID();
}
break;
}
return $fields;
}
public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
return PhabricatorMarkupEngine::summarize($text);
}
return null;
}
public function getBodyForFeed(PhabricatorFeedStory $story) {
$remarkup = $this->getRemarkupBodyForFeed($story);
if ($remarkup !== null) {
$remarkup = PhabricatorMarkupEngine::summarize($remarkup);
return new PHUIRemarkupView($this->viewer, $remarkup);
}
$old = $this->getOldValue();
$new = $this->getNewValue();
$body = null;
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$body = $story->getMarkupFieldOutput('comment/'.$this->getID());
}
break;
}
return $body;
}
public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) {
return null;
}
public function getActionStrength() {
if ($this->isInlineCommentTransaction()) {
return 0.25;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return 0.5;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if ($this->isSelfSubscription()) {
// Make this weaker than TYPE_COMMENT.
return 0.25;
}
if ($this->isApplicationAuthor()) {
// When applications (most often: Herald) change subscriptions it
// is very uninteresting.
return 0.000000001;
}
// In other cases, subscriptions are more interesting than comments
// (which are shown anyway) but less interesting than any other type of
// transaction.
return 0.75;
}
return 1.0;
}
public function isCommentTransaction() {
if ($this->hasComment()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return true;
}
return false;
}
public function isInlineCommentTransaction() {
return false;
}
public function getActionName() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('Commented On');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht('Changed Policy');
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht('Changed Subscribers');
default:
return pht('Updated');
}
}
public function getMailTags() {
return array();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionHasChangeDetails($this);
}
break;
}
return false;
}
public function hasChangeDetailsForMail() {
return $this->hasChangeDetails();
}
public function renderChangeDetailsForMail(PhabricatorUser $viewer) {
$view = $this->renderChangeDetails($viewer);
if ($view instanceof PhabricatorApplicationTransactionTextDiffDetailView) {
return $view->renderForMail();
}
return null;
}
public function renderChangeDetails(PhabricatorUser $viewer) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionChangeDetails($this, $viewer);
}
break;
}
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
public function renderTextCorpusChangeDetails(
PhabricatorUser $viewer,
$old,
$new) {
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setUser($viewer)
->setOldText($old)
->setNewText($new);
}
public function attachTransactionGroup(array $group) {
assert_instances_of($group, __CLASS__);
$this->transactionGroup = $group;
return $this;
}
public function getTransactionGroup() {
return $this->transactionGroup;
}
/**
* Should this transaction be visually grouped with an existing transaction
* group?
*
* @param list<PhabricatorApplicationTransaction> List of transactions.
* @return bool True to display in a group with the other transactions.
*/
public function shouldDisplayGroupWith(array $group) {
$this_source = null;
if ($this->getContentSource()) {
$this_source = $this->getContentSource()->getSource();
}
foreach ($group as $xaction) {
// Don't group transactions by different authors.
if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
return false;
}
// Don't group transactions for different objects.
if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
return false;
}
// Don't group anything into a group which already has a comment.
if ($xaction->isCommentTransaction()) {
return false;
}
// Don't group transactions from different content sources.
$other_source = null;
if ($xaction->getContentSource()) {
$other_source = $xaction->getContentSource()->getSource();
}
if ($other_source != $this_source) {
return false;
}
// Don't group transactions which happened more than 2 minutes apart.
$apart = abs($xaction->getDateCreated() - $this->getDateCreated());
if ($apart > (60 * 2)) {
return false;
}
// Don't group silent and nonsilent transactions together.
$is_silent = $this->getIsSilentTransaction();
if ($is_silent != $xaction->getIsSilentTransaction()) {
return false;
}
}
return true;
}
public function renderExtraInformationLink() {
$herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
if ($herald_xscript_id) {
return phutil_tag(
'a',
array(
'href' => '/herald/transcript/'.$herald_xscript_id.'/',
),
pht('View Herald Transcript'));
}
return null;
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$text = array();
$body = array();
foreach ($xactions as $xaction) {
$xaction_body = $xaction->getBodyForMail();
if ($xaction_body !== null) {
$body[] = $xaction_body;
}
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$old_target = $xaction->getRenderingTarget();
$new_target = self::TARGET_TEXT;
$xaction->setRenderingTarget($new_target);
if ($publisher->getRenderWithImpliedContext()) {
$text[] = $xaction->getTitle();
} else {
$text[] = $xaction->getTitleForFeed();
}
$xaction->setRenderingTarget($old_target);
}
$text = implode("\n", $text);
$body = implode("\n\n", $body);
return rtrim($text."\n\n".$body);
}
/**
* Test if this transaction is just a user subscribing or unsubscribing
* themselves.
*/
private function isSelfSubscription() {
$type = $this->getTransactionType();
if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
return false;
}
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($old, $new);
$rem = array_diff($new, $old);
if ((count($add) + count($rem)) != 1) {
// More than one user affected.
return false;
}
$affected_phid = head(array_merge($add, $rem));
if ($affected_phid != $this->getAuthorPHID()) {
// Affected user is someone else.
return false;
}
return true;
}
private function isApplicationAuthor() {
$author_phid = $this->getAuthorPHID();
$author_type = phid_get_type($author_phid);
$application_type = PhabricatorApplicationApplicationPHIDType::TYPECONST;
return ($author_type == $application_type);
}
private function getInterestingMoves(array $moves) {
// Remove moves which only shift the position of a task within a column.
foreach ($moves as $key => $move) {
$from_phids = array_fuse($move['fromColumnPHIDs']);
if (isset($from_phids[$move['columnPHID']])) {
unset($moves[$key]);
}
}
return $moves;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'Transactions are visible to users that can see the object which was '.
'acted upon. Some transactions - in particular, comments - are '.
'editable by the transaction author.');
}
public function getModularType() {
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$comment_template = null;
try {
$comment_template = $this->getApplicationTransactionCommentObject();
} catch (Exception $ex) {
// Continue; no comments for these transactions.
}
if ($comment_template) {
$comments = $comment_template->loadAllWhere(
'transactionPHID = %s',
$this->getPHID());
foreach ($comments as $comment) {
$engine->destroyObject($comment);
}
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
index eaf7d029c..3d81e02c3 100644
--- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
@@ -1,359 +1,395 @@
<?php
abstract class PhabricatorModularTransactionType
extends Phobject {
private $storage;
private $viewer;
private $editor;
final public function getTransactionTypeConstant() {
return $this->getPhobjectClassConstant('TRANSACTIONTYPE');
}
public function generateOldValue($object) {
throw new PhutilMethodNotImplementedException();
}
public function generateNewValue($object, $value) {
return $value;
}
public function validateTransactions($object, array $xactions) {
return array();
}
public function willApplyTransactions($object, array $xactions) {
return;
}
public function applyInternalEffects($object, $value) {
return;
}
public function applyExternalEffects($object, $value) {
return;
}
public function getTransactionHasEffect($object, $old, $new) {
return ($old !== $new);
}
public function extractFilePHIDs($object, $value) {
return array();
}
public function shouldHide() {
return false;
}
public function shouldHideForFeed() {
return false;
}
public function shouldHideForMail() {
return false;
}
public function getIcon() {
return null;
}
public function getTitle() {
return null;
}
public function getTitleForFeed() {
return null;
}
public function getActionName() {
return null;
}
public function getActionStrength() {
return null;
}
public function getColor() {
return null;
}
public function hasChangeDetailView() {
return false;
}
public function newChangeDetailView() {
return null;
}
public function getMailDiffSectionHeader() {
return pht('EDIT DETAILS');
}
public function newRemarkupChanges() {
return array();
}
public function mergeTransactions(
$object,
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
return null;
}
final public function setStorage(
PhabricatorApplicationTransaction $xaction) {
$this->storage = $xaction;
return $this;
}
private function getStorage() {
return $this->storage;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final protected function getViewer() {
return $this->viewer;
}
final public function getActor() {
return $this->getEditor()->getActor();
}
final public function getActingAsPHID() {
return $this->getEditor()->getActingAsPHID();
}
final public function setEditor(
PhabricatorApplicationTransactionEditor $editor) {
$this->editor = $editor;
return $this;
}
final protected function getEditor() {
if (!$this->editor) {
throw new PhutilInvalidStateException('setEditor');
}
return $this->editor;
}
final protected function hasEditor() {
return (bool)$this->editor;
}
final protected function getAuthorPHID() {
return $this->getStorage()->getAuthorPHID();
}
final protected function getObjectPHID() {
return $this->getStorage()->getObjectPHID();
}
final protected function getObject() {
return $this->getStorage()->getObject();
}
final protected function getOldValue() {
return $this->getStorage()->getOldValue();
}
final protected function getNewValue() {
return $this->getStorage()->getNewValue();
}
final protected function renderAuthor() {
$author_phid = $this->getAuthorPHID();
return $this->getStorage()->renderHandleLink($author_phid);
}
final protected function renderObject() {
$object_phid = $this->getObjectPHID();
return $this->getStorage()->renderHandleLink($object_phid);
}
final protected function renderHandle($phid) {
$viewer = $this->getViewer();
$display = $viewer->renderHandle($phid);
if ($this->isTextMode()) {
$display->setAsText(true);
}
return $display;
}
final protected function renderOldHandle() {
return $this->renderHandle($this->getOldValue());
}
final protected function renderNewHandle() {
return $this->renderHandle($this->getNewValue());
}
final protected function renderHandleList(array $phids) {
$viewer = $this->getViewer();
$display = $viewer->renderHandleList($phids)
->setAsInline(true);
if ($this->isTextMode()) {
$display->setAsText(true);
}
return $display;
}
final protected function renderValue($value) {
if ($this->isTextMode()) {
return sprintf('"%s"', $value);
}
return phutil_tag(
'span',
array(
'class' => 'phui-timeline-value',
),
$value);
}
final protected function renderValueList(array $values) {
$result = array();
foreach ($values as $value) {
$result[] = $this->renderValue($value);
}
if ($this->isTextMode()) {
return implode(', ', $result);
}
return phutil_implode_html(', ', $result);
}
final protected function renderOldValue() {
return $this->renderValue($this->getOldValue());
}
final protected function renderNewValue() {
return $this->renderValue($this->getNewValue());
}
final protected function renderDate($epoch) {
$viewer = $this->getViewer();
// We accept either epoch timestamps or dictionaries describing a
// PhutilCalendarDateTime.
if (is_array($epoch)) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch)
->setViewerTimezone($viewer->getTimezoneIdentifier());
$all_day = $datetime->getIsAllDay();
$epoch = $datetime->getEpoch();
} else {
$all_day = false;
}
if ($all_day) {
$display = phabricator_date($epoch, $viewer);
} else if ($this->isRenderingTargetExternal()) {
// When rendering to text, we explicitly render the offset from UTC to
// provide context to the date: the mail may be generating with the
// server's settings, or the user may later refer back to it after
// changing timezones.
$display = phabricator_datetimezone($epoch, $viewer);
} else {
$display = phabricator_datetime($epoch, $viewer);
}
return $this->renderValue($display);
}
final protected function renderOldDate() {
return $this->renderDate($this->getOldValue());
}
final protected function renderNewDate() {
return $this->renderDate($this->getNewValue());
}
final protected function newError($title, $message, $xaction = null) {
return new PhabricatorApplicationTransactionValidationError(
$this->getTransactionTypeConstant(),
$title,
$message,
$xaction);
}
final protected function newRequiredError($message, $xaction = null) {
return $this->newError(pht('Required'), $message, $xaction)
->setIsMissingFieldError(true);
}
final protected function newInvalidError($message, $xaction = null) {
return $this->newError(pht('Invalid'), $message, $xaction);
}
final protected function isNewObject() {
return $this->getEditor()->getIsNewObject();
}
final protected function isEmptyTextTransaction($value, array $xactions) {
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
}
return !strlen($value);
}
/**
* When rendering to external targets (Email/Asana/etc), we need to include
* more information that users can't obtain later.
*/
final protected function isRenderingTargetExternal() {
// Right now, this is our best proxy for this:
return $this->isTextMode();
// "TARGET_TEXT" means "EMail" and "TARGET_HTML" means "Web".
}
final protected function isTextMode() {
$target = $this->getStorage()->getRenderingTarget();
return ($target == PhabricatorApplicationTransaction::TARGET_TEXT);
}
final protected function newRemarkupChange() {
return id(new PhabricatorTransactionRemarkupChange())
->setTransaction($this->getStorage());
}
final protected function isCreateTransaction() {
return $this->getStorage()->getIsCreateTransaction();
}
final protected function getPHIDList(array $old, array $new) {
$editor = $this->getEditor();
return $editor->getPHIDList($old, $new);
}
public function getMetadataValue($key, $default = null) {
return $this->getStorage()->getMetadataValue($key, $default);
}
public function loadTransactionTypeConduitData(array $xactions) {
return null;
}
public function getTransactionTypeForConduit($xaction) {
return null;
}
public function getFieldValuesForConduit($xaction, $data) {
return array();
}
+ protected function requireApplicationCapability($capability) {
+ $application_class = $this->getEditor()->getEditorApplicationClass();
+ $application = newv($application_class, array());
+
+ PhabricatorPolicyFilter::requireCapability(
+ $this->getActor(),
+ $application,
+ $capability);
+ }
+
+ /**
+ * Get a list of capabilities the actor must have on the object to apply
+ * a transaction to it.
+ *
+ * Usually, you should use this to reduce capability requirements when a
+ * transaction (like leaving a Conpherence thread) can be applied without
+ * having edit permission on the object. You can override this method to
+ * remove the CAN_EDIT requirement, or to replace it with a different
+ * requirement.
+ *
+ * If you are increasing capability requirements and need to add an
+ * additional capability or policy requirement above and beyond CAN_EDIT, it
+ * is usually better implemented as a validation check.
+ *
+ * @param object Object being edited.
+ * @param PhabricatorApplicationTransaction Transaction being applied.
+ * @return null|const|list<const> A capability constant (or list of
+ * capability constants) which the actor must have on the object. You can
+ * return `null` as a shorthand for "no capabilities are required".
+ */
+ public function getRequiredCapabilities(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ }
+
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
index a5dcfcb4a..7469004e1 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
@@ -1,557 +1,561 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $submitButtonName;
private $action;
private $previewPanelID;
private $previewTimelineID;
private $previewToggleID;
private $formID;
private $statusID;
private $commentID;
private $draft;
private $requestURI;
private $showPreview = true;
private $objectPHID;
private $headerText;
private $noPermission;
private $fullWidth;
private $infoView;
private $editEngineLock;
private $currentVersion;
private $versionedDraft;
private $commentActions;
private $commentActionGroups = array();
private $transactionTimeline;
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setShowPreview($show_preview) {
$this->showPreview = $show_preview;
return $this;
}
public function getShowPreview() {
return $this->showPreview;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setCurrentVersion($current_version) {
$this->currentVersion = $current_version;
return $this;
}
public function getCurrentVersion() {
return $this->currentVersion;
}
public function setVersionedDraft(
PhabricatorVersionedDraft $versioned_draft) {
$this->versionedDraft = $versioned_draft;
return $this;
}
public function getVersionedDraft() {
return $this->versionedDraft;
}
public function setDraft(PhabricatorDraft $draft) {
$this->draft = $draft;
return $this;
}
public function getDraft() {
return $this->draft;
}
public function setSubmitButtonName($submit_button_name) {
$this->submitButtonName = $submit_button_name;
return $this;
}
public function getSubmitButtonName() {
return $this->submitButtonName;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function getAction() {
return $this->action;
}
public function setHeaderText($text) {
$this->headerText = $text;
return $this;
}
public function setFullWidth($fw) {
$this->fullWidth = $fw;
return $this;
}
public function setInfoView(PHUIInfoView $info_view) {
$this->infoView = $info_view;
return $this;
}
public function getInfoView() {
return $this->infoView;
}
public function setCommentActions(array $comment_actions) {
assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction');
$this->commentActions = $comment_actions;
return $this;
}
public function getCommentActions() {
return $this->commentActions;
}
public function setCommentActionGroups(array $groups) {
assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup');
$this->commentActionGroups = $groups;
return $this;
}
public function getCommentActionGroups() {
return $this->commentActionGroups;
}
public function setNoPermission($no_permission) {
$this->noPermission = $no_permission;
return $this;
}
public function getNoPermission() {
return $this->noPermission;
}
public function setEditEngineLock(PhabricatorEditEngineLock $lock) {
$this->editEngineLock = $lock;
return $this;
}
public function getEditEngineLock() {
return $this->editEngineLock;
}
public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) {
$timeline->setQuoteTargetID($this->getCommentID());
if ($this->getNoPermission() || $this->getEditEngineLock()) {
$timeline->setShouldTerminate(true);
}
$this->transactionTimeline = $timeline;
return $this;
}
public function render() {
if ($this->getNoPermission()) {
return null;
}
$lock = $this->getEditEngineLock();
if ($lock) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$lock->getLockedObjectDisplayText(),
));
}
$user = $this->getUser();
if (!$user->isLoggedIn()) {
$uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView())
->setFlush(true)
->appendChild(
javelin_tag(
'a',
array(
'class' => 'login-to-comment button',
'href' => $uri,
),
pht('Log In to Comment')));
}
$data = array();
$comment = $this->renderCommentPanel();
if ($this->getShowPreview()) {
$preview = $this->renderPreviewPanel();
} else {
$preview = null;
}
if (!$this->getCommentActions()) {
Javelin::initBehavior(
'phabricator-transaction-comment-form',
array(
'formID' => $this->getFormID(),
'timelineID' => $this->getPreviewTimelineID(),
'panelID' => $this->getPreviewPanelID(),
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
));
}
require_celerity_resource('phui-comment-form-css');
$image_uri = $user->getProfileImageURI();
$image = phutil_tag(
'div',
array(
'style' => 'background-image: url('.$image_uri.')',
- 'class' => 'phui-comment-image',
+ 'class' => 'phui-comment-image visual-only',
));
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge',
),
'');
$badge_view = $this->renderBadgeView();
$comment_box = id(new PHUIObjectBoxView())
->setFlush(true)
->addClass('phui-comment-form-view')
->addSigil('phui-comment-form')
->appendChild($image)
->appendChild($badge_view)
->appendChild($wedge)
->appendChild($comment);
return array($comment_box, $preview);
}
private function renderCommentPanel() {
$draft_comment = '';
$draft_key = null;
if ($this->getDraft()) {
$draft_comment = $this->getDraft()->getDraft();
$draft_key = $this->getDraft()->getDraftKey();
}
$versioned_draft = $this->getVersionedDraft();
if ($versioned_draft) {
$draft_comment = $versioned_draft->getProperty('comment', '');
}
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID', 'render');
}
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$version_value = $this->getCurrentVersion();
$form = id(new AphrontFormView())
->setUser($this->getUser())
->addSigil('transaction-append')
->setWorkflow(true)
->setFullWidth($this->fullWidth)
->setMetadata(
array(
'objectPHID' => $this->getObjectPHID(),
))
->setAction($this->getAction())
->setID($this->getFormID())
->addHiddenInput('__draft__', $draft_key)
->addHiddenInput($version_key, $version_value);
$comment_actions = $this->getCommentActions();
if ($comment_actions) {
$action_map = array();
$type_map = array();
$comment_actions = mpull($comment_actions, null, 'getKey');
$draft_actions = array();
$draft_keys = array();
if ($versioned_draft) {
$draft_actions = $versioned_draft->getProperty('actions', array());
if (!is_array($draft_actions)) {
$draft_actions = array();
}
foreach ($draft_actions as $action) {
$type = idx($action, 'type');
$comment_action = idx($comment_actions, $type);
if (!$comment_action) {
continue;
}
$value = idx($action, 'value');
$comment_action->setValue($value);
$draft_keys[] = $type;
}
}
foreach ($comment_actions as $key => $comment_action) {
$key = $comment_action->getKey();
+ $label = $comment_action->getLabel();
+
+
$action_map[$key] = array(
'key' => $key,
- 'label' => $comment_action->getLabel(),
+ 'label' => $label,
'type' => $comment_action->getPHUIXControlType(),
'spec' => $comment_action->getPHUIXControlSpecification(),
'initialValue' => $comment_action->getInitialValue(),
'groupKey' => $comment_action->getGroupKey(),
'conflictKey' => $comment_action->getConflictKey(),
+ 'auralLabel' => pht('Remove Action: %s', $label),
);
$type_map[$key] = $comment_action;
}
$options = $this->newCommentActionOptions($action_map);
$action_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$place_id = celerity_generate_unique_node_id();
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'editengine.actions',
'id' => $input_id,
)));
$invisi_bar = phutil_tag(
'div',
array(
'id' => $place_id,
'class' => 'phui-comment-control-stack',
));
$action_select = id(new AphrontFormSelectControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-action-control')
->setID($action_id)
->setOptions($options);
$action_bar = phutil_tag(
'div',
array(
'class' => 'phui-comment-action-bar grouped',
),
array(
$action_select,
));
$form->appendChild($action_bar);
$info_view = $this->getInfoView();
if ($info_view) {
$form->appendChild($info_view);
}
$form->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions');
Javelin::initBehavior(
'comment-actions',
array(
'actionID' => $action_id,
'inputID' => $input_id,
'formID' => $this->getFormID(),
'placeID' => $place_id,
'panelID' => $this->getPreviewPanelID(),
'timelineID' => $this->getPreviewTimelineID(),
'actions' => $action_map,
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
'drafts' => $draft_keys,
));
}
$submit_button = id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->setValue($this->getSubmitButtonName());
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setID($this->getCommentID())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-textarea-control')
->setCanPin(true)
->setName('comment')
->setUser($this->getUser())
->setValue($draft_comment))
->appendChild(
id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->setValue($this->getSubmitButtonName()));
return $form;
}
private function renderPreviewPanel() {
$preview = id(new PHUITimelineView())
->setID($this->getPreviewTimelineID());
return phutil_tag(
'div',
array(
'id' => $this->getPreviewPanelID(),
'style' => 'display: none',
'class' => 'phui-comment-preview-view',
),
$preview);
}
private function getPreviewPanelID() {
if (!$this->previewPanelID) {
$this->previewPanelID = celerity_generate_unique_node_id();
}
return $this->previewPanelID;
}
private function getPreviewTimelineID() {
if (!$this->previewTimelineID) {
$this->previewTimelineID = celerity_generate_unique_node_id();
}
return $this->previewTimelineID;
}
public function setFormID($id) {
$this->formID = $id;
return $this;
}
private function getFormID() {
if (!$this->formID) {
$this->formID = celerity_generate_unique_node_id();
}
return $this->formID;
}
private function getStatusID() {
if (!$this->statusID) {
$this->statusID = celerity_generate_unique_node_id();
}
return $this->statusID;
}
private function getCommentID() {
if (!$this->commentID) {
$this->commentID = celerity_generate_unique_node_id();
}
return $this->commentID;
}
private function newCommentActionOptions(array $action_map) {
$options = array();
$options['+'] = pht('Add Action...');
// Merge options into groups.
$groups = array();
foreach ($action_map as $key => $item) {
$group_key = $item['groupKey'];
if (!isset($groups[$group_key])) {
$groups[$group_key] = array();
}
$groups[$group_key][$key] = $item;
}
$group_specs = $this->getCommentActionGroups();
$group_labels = mpull($group_specs, 'getLabel', 'getKey');
// Reorder groups to put them in the same order as the recognized
// group definitions.
$groups = array_select_keys($groups, array_keys($group_labels)) + $groups;
// Move options with no group to the end.
$default_group = idx($groups, '');
if ($default_group) {
unset($groups['']);
$groups[''] = $default_group;
}
foreach ($groups as $group_key => $group_items) {
if (strlen($group_key)) {
$group_label = idx($group_labels, $group_key, $group_key);
$options[$group_label] = ipull($group_items, 'label');
} else {
foreach ($group_items as $key => $item) {
$options[$key] = $item['label'];
}
}
}
return $options;
}
private function renderBadgeView() {
$user = $this->getUser();
$can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorBadgesApplication',
$user);
if (!$can_use_badges) {
return null;
}
// Pull Badges from UserCache
$badges = $user->getRecentBadgeAwards();
$badge_view = null;
if ($badges) {
$badge_list = array();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge['id'].'/');
$badge_list[] = $badge_view;
}
$flex = new PHUIBadgeBoxView();
$flex->addItems($badge_list);
$flex->setCollapsed(true);
$badge_view = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
return $badge_view;
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
index 27b30a627..a7a1f55ca 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
@@ -1,159 +1,159 @@
<?php
final class PhabricatorTypeaheadFunctionHelpController
extends PhabricatorTypeaheadDatasourceController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$class = $request->getURIData('class');
$sources = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorTypeaheadDatasource')
->execute();
if (!isset($sources[$class])) {
return new Aphront404Response();
}
$raw_parameters = $request->getStr('parameters');
if ($raw_parameters) {
$parameters = phutil_json_decode($raw_parameters);
} else {
$parameters = array();
}
$source = id(clone $sources[$class])
->setParameters($parameters);
// This can fail for some types of datasources (like the custom field proxy
// datasources) if the "parameters" are wrong. Just fail cleanly instead
// of fataling.
try {
$application_class = $source->getDatasourceApplicationClass();
} catch (Exception $ex) {
return new Aphront404Response();
}
if ($application_class) {
$result = id(new PhabricatorApplicationQuery())
->setViewer($this->getViewer())
->withClasses(array($application_class))
->execute();
if (!$result) {
return new Aphront404Response();
}
}
$source->setViewer($viewer);
$title = pht('Typeahead Function Help');
$functions = $source->getAllDatasourceFunctions();
ksort($functions);
$content = array();
$content[] = '= '.pht('Overview');
$content[] = pht(
'Typeahead functions are an advanced feature which allow you to build '.
'more powerful queries. This document explains functions available '.
'for the selected control.'.
"\n\n".
'For general help with search, see the [[ %s | Search User Guide ]] in '.
'the documentation.'.
"\n\n".
'Note that different controls support //different// functions '.
'(depending on what the control is doing), so these specific functions '.
'may not work everywhere. You can always check the help for a control '.
'to review which functions are available for that control.',
PhabricatorEnv::getDoclink('Search User Guide'));
$table = array();
$table_header = array(
pht('Function'),
pht('Token Name'),
pht('Summary'),
);
$table[] = '| '.implode(' | ', $table_header).' |';
$table[] = '|---|---|---|';
foreach ($functions as $function => $spec) {
$spec = $spec + array(
'summary' => null,
'arguments' => null,
);
if (idx($spec, 'arguments')) {
$signature = '**'.$function.'(**//'.$spec['arguments'].'//**)**';
} else {
$signature = '**'.$function.'()**';
}
$name = idx($spec, 'name', '');
$summary = idx($spec, 'summary', '');
$table[] = '| '.$signature.' | '.$name.' | '.$summary.' |';
}
$table = implode("\n", $table);
$content[] = '= '.pht('Function Quick Reference');
$content[] = pht(
'This table briefly describes available functions for this control. '.
'For details on a particular function, see the corresponding section '.
'below.');
$content[] = $table;
$content[] = '= '.pht('Using Typeahead Functions');
$content[] = pht(
"In addition to typing user and project names to build queries, you can ".
"also type the names of special functions which give you more options ".
"and the ability to express more complex queries.\n\n".
"Functions have an internal name (like `%s`) and a human-readable name, ".
"like `Current Viewer`. In general, you can type either one to select ".
"the function. You can also click the {nav icon=search} button on any ".
"typeahead control to browse available functions and find this ".
"documentation.\n\n".
"This documentation uses the internal names to make it clear where ".
"tokens begin and end. Specifically, you will find queries written ".
"out like this in the documentation:\n\n%s\n\n".
"When this query is actually shown in the control, it will look more ".
"like this:\n\n%s",
'viewer()',
'> viewer(), alincoln',
'> {nav Current Viewer} {nav alincoln (Abraham Lincoln)}');
$middot = "\xC2\xB7";
foreach ($functions as $function => $spec) {
$arguments = idx($spec, 'arguments', '');
$name = idx($spec, 'name');
$content[] = '= '.$function.'('.$arguments.') '.$middot.' '.$name;
$content[] = $spec['description'];
}
$content = implode("\n\n", $content);
$content_box = new PHUIRemarkupView($viewer, $content);
$header = id(new PHUIHeaderView())
->setHeader($title);
- $document = id(new PHUIDocumentViewPro())
+ $document = id(new PHUIDocumentView())
->setHeader($header)
->appendChild($content_box);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Function Help'));
$crumbs->setBorder(true);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($document);
}
}
diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner
index 5de8429c1..a33134fe4 100644
--- a/src/docs/user/configuration/configuring_outbound_email.diviner
+++ b/src/docs/user/configuration/configuring_outbound_email.diviner
@@ -1,330 +1,334 @@
@title Configuring Outbound Email
@group config
Instructions for configuring Phabricator to send mail.
Overview
========
Phabricator can send outbound email through several different mail services,
including a local mailer or various third-party services. Options include:
| Send Mail With | Setup | Cost | Inbound | Notes |
|---------|-------|------|---------|-------|
| Mailgun | Easy | Cheap | Yes | Recommended |
| Postmark | Easy | Cheap | Yes | Recommended |
| Amazon SES | Easy | Cheap | No | Recommended |
| SendGrid | Medium | Cheap | Yes | Discouraged |
| External SMTP | Medium | Varies | No | Gmail, etc. |
| Local SMTP | Hard | Free | No | sendmail, postfix, etc |
| Custom | Hard | Free | No | Write a custom mailer for some other service. |
| Drop in a Hole | Easy | Free | No | Drops mail in a deep, dark hole. |
See below for details on how to select and configure mail delivery for each
mailer.
Overall, Mailgun and SES are much easier to set up, and using one of them is
recommended. In particular, Mailgun will also let you set up inbound email
easily.
If you have some internal mail service you'd like to use you can also
write a custom mailer, but this requires digging into the code.
Phabricator sends mail in the background, so the daemons need to be running for
it to be able to deliver mail. You should receive setup warnings if they are
not. For more information on using daemons, see
@{article:Managing Daemons with phd}.
Basics
======
Regardless of how outbound email is delivered, you should configure these keys
in your configuration:
- **metamta.default-address** determines where mail is sent "From" by
default. If your domain is `example.org`, set this to something like
`noreply@example.org`.
- **metamta.domain** should be set to your domain, e.g. `example.org`.
- **metamta.can-send-as-user** should be left as `false` in most cases,
but see the documentation for details.
Configuring Mailers
===================
Configure one or more mailers by listing them in the the `cluster.mailers`
configuration option. Most installs only need to configure one mailer, but you
can configure multiple mailers to provide greater availability in the event of
a service disruption.
A valid `cluster.mailers` configuration looks something like this:
```lang=json
[
{
"key": "mycompany-mailgun",
"type": "mailgun",
"options": {
"domain": "mycompany.com",
"api-key": "..."
}
},
...
]
```
The supported keys for each mailer are:
- `key`: Required string. A unique name for this mailer.
- `type`: Required string. Identifies the type of mailer. See below for
options.
- `priority`: Optional string. Advanced option which controls load balancing
and failover behavior. See below for details.
- `options`: Optional map. Additional options for the mailer type.
+ - `inbound`: Optional bool. Use `false` to prevent this mailer from being
+ used to receive inbound mail.
+ - `outbound`: Optional bool. Use `false` to prevent this mailer from being
+ used to send outbound mail.
The `type` field can be used to select these third-party mailers:
- `mailgun`: Use Mailgun.
- `ses`: Use Amazon SES.
- `sendgrid`: Use Sendgrid.
It also supports these local mailers:
- `sendmail`: Use the local `sendmail` binary.
- `smtp`: Connect directly to an SMTP server.
- `test`: Internal mailer for testing. Does not send mail.
You can also write your own mailer by extending
`PhabricatorMailImplementationAdapter`.
Once you've selected a mailer, find the corresponding section below for
instructions on configuring it.
Setting Complex Configuration
=============================
Mailers can not be edited from the web UI. If mailers could be edited from
the web UI, it would give an attacker who compromised an administrator account
a lot of power: they could redirect mail to a server they control and then
intercept mail for any other account, including password reset mail.
For more information about locked configuration options, see
@{article:Configuration Guide: Locked and Hidden Configuration}.
Setting `cluster.mailers` from the command line using `bin/config set` can be
tricky because of shell escaping. The easiest way to do it is to use the
`--stdin` flag. First, put your desired configuration in a file like this:
```lang=json, name=mailers.json
[
{
"key": "test-mailer",
"type": "test"
}
]
```
Then set the value like this:
```
phabricator/ $ ./bin/config set --stdin < mailers.json
```
For alternatives and more information on configuration, see
@{article:Configuration User Guide: Advanced Configuration}
Mailer: Mailgun
===============
Mailgun is a third-party email delivery service. You can learn more at
<http://www.mailgun.com>. Mailgun is easy to configure and works well.
To use this mailer, set `type` to `mailgun`, then configure these `options`:
- `api-key`: Required string. Your Mailgun API key.
- `domain`: Required string. Your Mailgun domain.
Mailer: Postmark
================
Postmark is a third-party email delivery serivice. You can learn more at
<https://www.postmarkapp.com/>.
To use this mailer, set `type` to `postmark`, then configure these `options`:
- `access-token`: Required string. Your Postmark access token.
- `inbound-addresses`: Optional list<string>. Address ranges which you
will accept inbound Postmark HTTP webook requests from.
The default address list is preconfigured with Postmark's address range, so
you generally will not need to set or adjust it.
The option accepts a list of CIDR ranges, like `1.2.3.4/16` (IPv4) or
`::ffff:0:0/96` (IPv6). The default ranges are:
```lang=json
[
"50.31.156.6/32"
]
```
The default address ranges were last updated in February 2018, and were
documented at: <https://postmarkapp.com/support/article/800-ips-for-firewalls>
Mailer: Amazon SES
==================
Amazon SES is Amazon's cloud email service. You can learn more at
<http://aws.amazon.com/ses/>.
To use this mailer, set `type` to `ses`, then configure these `options`:
- `access-key`: Required string. Your Amazon SES access key.
- `secret-key`: Required string. Your Amazon SES secret key.
- `endpoint`: Required string. Your Amazon SES endpoint.
NOTE: Amazon SES **requires you to verify your "From" address**. Configure
which "From" address to use by setting "`metamta.default-address`" in your
config, then follow the Amazon SES verification process to verify it. You
won't be able to send email until you do this!
Mailer: SendGrid
================
SendGrid is a third-party email delivery service. You can learn more at
<http://sendgrid.com/>.
You can configure SendGrid in two ways: you can send via SMTP or via the REST
API. To use SMTP, configure Phabricator to use an `smtp` mailer.
To use the REST API mailer, set `type` to `sendgrid`, then configure
these `options`:
- `api-user`: Required string. Your SendGrid login name.
- `api-key`: Required string. Your SendGrid API key.
NOTE: Users have experienced a number of odd issues with SendGrid, compared to
fewer issues with other mailers. We discourage SendGrid unless you're already
using it.
Mailer: Sendmail
================
This requires a `sendmail` binary to be installed on
the system. Most MTAs (e.g., sendmail, qmail, postfix) should do this, but your
machine may not have one installed by default. For install instructions, consult
the documentation for your favorite MTA.
Since you'll be sending the mail yourself, you are subject to things like SPF
rules, blackholes, and MTA configuration which are beyond the scope of this
document. If you can already send outbound email from the command line or know
how to configure it, this option is straightforward. If you have no idea how to
do any of this, strongly consider using Mailgun or Amazon SES instead.
To use this mailer, set `type` to `sendmail`. There are no `options` to
configure.
Mailer: STMP
============
You can use this adapter to send mail via an external SMTP server, like Gmail.
To use this mailer, set `type` to `smtp`, then configure these `options`:
- `host`: Required string. The hostname of your SMTP server.
- `port`: Optional int. The port to connect to on your SMTP server.
- `user`: Optional string. Username used for authentication.
- `password`: Optional string. Password for authentication.
- `protocol`: Optional string. Set to `tls` or `ssl` if necessary. Use
`ssl` for Gmail.
Disable Mail
============
To disable mail, just don't configure any mailers.
Testing and Debugging Outbound Email
====================================
You can use the `bin/mail` utility to test, debug, and examine outbound mail. In
particular:
phabricator/ $ ./bin/mail list-outbound # List outbound mail.
phabricator/ $ ./bin/mail show-outbound # Show details about messages.
phabricator/ $ ./bin/mail send-test # Send test messages.
Run `bin/mail help <command>` for more help on using these commands.
You can monitor daemons using the Daemon Console (`/daemon/`, or click
**Daemon Console** from the homepage).
Priorities
==========
By default, Phabricator will try each mailer in order: it will try the first
mailer first. If that fails (for example, because the service is not available
at the moment) it will try the second mailer, and so on.
If you want to load balance between multiple mailers instead of using one as
a primary, you can set `priority`. Phabricator will start with mailers in the
highest priority group and go through them randomly, then fall back to the
next group.
For example, if you have two SMTP servers and you want to balance requests
between them and then fall back to Mailgun if both fail, configure priorities
like this:
```lang=json
[
{
"key": "smtp-uswest",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "smtp-useast",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "mailgun-fallback",
"type": "mailgun",
"options": "..."
}
}
```
Phabricator will start with servers in the highest priority group (the group
with the **largest** `priority` number). In this example, the highest group is
`300`, which has the two SMTP servers. They'll be tried in random order first.
If both fail, Phabricator will move on to the next priority group. In this
example, there are no other priority groups.
If it still hasn't sent the mail, Phabricator will try servers which are not
in any priority group, in the configured order. In this example there is
only one such server, so it will try to send via Mailgun.
Next Steps
==========
Continue by:
- @{article:Configuring Inbound Email} so users can reply to email they
receive about revisions and tasks to interact with them; or
- learning about daemons with @{article:Managing Daemons with phd}; or
- returning to the @{article:Configuration Guide}.
diff --git a/src/docs/user/userguide/diffusion_existing.diviner b/src/docs/user/userguide/diffusion_existing.diviner
index bf09d9ef1..341146f0f 100644
--- a/src/docs/user/userguide/diffusion_existing.diviner
+++ b/src/docs/user/userguide/diffusion_existing.diviner
@@ -1,68 +1,68 @@
@title Diffusion User Guide: Existing Repositories
@group userguide
Quick guide for importing or observing existing repositories.
Overview
========
If you have an existing repository, you can observe or import it into
Diffusion.
Observing a repository creates a read-only copy in Phabricator that is kept
up to date by continuously importing new changes.
Importing a repository creates a read-write copy.
This document is a quick guide to getting started. For an overview of
Diffusion, see @{article:Diffusion User Guide}. For a more detailed guide
about managing repositories and URIs in Diffusion, see
@{article:Diffusion User Guide: URIs}.
Observing Repositories
======================
To observe an existing repository:
- Create a repository in Diffusion, but do not activate it yet.
- Add the URI for the existing repository you wish to observe in the
**URIs** section, in **Observe** mode.
- Activate the repository in Diffusion.
This creates a read-only copy of the repository in Phabricator. Phabricator
will keep its copy in sync with the remote by periodically polling the remote
for changes.
For more details, see @{article:Diffusion User Guide: URIs}.
Importing Repositories
======================
There are two primary ways to import an existing repository:
**Observe First**: In Git or Mercurial, you can observe the repository first.
Once the import completes, change the "I/O Type" on the **Observe** URI to
"No I/O" mode to automatically convert it into a hosted repository.
-**Push to Empty Repository**: Create an activate an empty repository, then push
+**Push to Empty Repository**: Create and activate an empty repository, then push
all of your changes to the empty repository.
In Git and Mercurial, you can do this with `git push` or `hg push`.
In Subversion, you can do this with `svnsync`.
For more details, see @{article:Diffusion User Guide: URIs}.
Next Steps
==========
Continue by:
- reading an overview of Diffusion in
@{article:Diffusion User Guide}; or
- learning more about managing remote repository URIs in
@{article:Diffusion User Guide: URIs}.
diff --git a/src/docs/user/userguide/owners.diviner b/src/docs/user/userguide/owners.diviner
index 35260b08a..95a388255 100644
--- a/src/docs/user/userguide/owners.diviner
+++ b/src/docs/user/userguide/owners.diviner
@@ -1,159 +1,174 @@
@title Owners User Guide
@group userguide
Group files in a codebase into packages and define ownership.
Overview
========
The Owners application allows you to group files in a codebase (or across
codebases) into packages. This can make it easier to reference a module or
subsystem in other applications, like Herald.
Creating a Package
==================
To create a package, choose a name and add some files which belong to the
package. For example, you might define an "iOS Application" package by
including these paths:
/conf/ios/
/src/ios/
/shared/assets/mobile/
Any files in those directories are considered to be part of the package, and
you can now conveniently refer to them (for example, in a Herald rule) by
referring to the package instead of copy/pasting a huge regular expression
into a bunch of places.
If new source files are later added, or the scope of the package otherwise
expands or contracts, you can edit the package definition to keep things
updated.
You can use "exclude" paths to ignore subdirectories which would otherwise
be considered part of the package. For example, you might exclude a path
like this:
/conf/ios/generated/
Perhaps that directory contains some generated configuration which frequently
changes, and which you aren't concerned about.
After creating a package, files the package contains will be identified as
belonging to the package when you look at them in Diffusion, or look at changes
which affect them in Diffusion or Differential.
Dominion
========
The **Dominion** option allows you to control how ownership cascades when
multiple packages own a path. The dominion rules are:
**Strong Dominion.** This is the default. In this mode, the package will always
own all files matching its configured paths, even if another package also owns
them.
For example, if the package owns `a/`, it will always own `a/b/c.z` even if
another package owns `a/b/`. In this case, both packages will own `a/b/c.z`.
This mode prevents users from stealing files away from the package by defining
more narrow ownership rules in new packages, but enforces hierarchical
ownership rules.
**Weak Dominion.** In this mode, the package will only own files which do not
match a more specific path in another package.
For example, if the package owns `a/` but another package owns `a/b/`, the
package will no longer consider `a/b/c.z` to be a file it owns because another
package matches the path with a more specific rule.
This mode lets you to define rules without implicit hierarchical ownership,
but allows users to steal files away from a package by defining a more
specific package.
For more details on files which match multiple packages, see
"Files in Multiple Packages", below.
Auto Review
===========
You can configure **Auto Review** for packages. When a new code review is
created in Differential which affects code in a package, the package can
automatically be added as a subscriber or reviewer.
The available settings allow you to take these actions:
- **Review Changes**: This package will be added to reviews as a reviewer.
Reviews will appear on the dashboards of package owners.
- **Review Changes (Blocking)** This package will be added to reviews as a
blocking reviewer. A package owner will be required to accept changes
before they may land.
- **Subscribe to Changes**: This package will be added to reviews as a
subscriber. Owners will be notified of changes, but not required to act.
If you select the **With Non-Owner Author** option for these actions, the
action will not trigger if the author of the revision is a package owner. This
mode may be helpful if you are using Owners mostly to make sure that someone
who is qualified is involved in each change to a piece of code.
If you select the **All** option for these actions, the action will always
trigger even if the author is a package owner. This mode may be helpful if you
are using Owners mostly to suggest reviewers.
These rules do not trigger if the package has been archived.
The intent of this feature is to make it easy to configure simple, reasonable
behaviors. If you want more tailored or specific triggers, you can write more
powerful rules by using Herald.
Auditing
========
You can automatically trigger audits on unreviewed code by configuring
**Auditing**. The available settings are:
- **Disabled**: Do not trigger audits.
- **Enabled**: Trigger audits.
When enabled, audits are triggered for commits which:
- affect code owned by the package;
- were not authored by a package owner; and
- were not accepted by a package owner.
Audits do not trigger if the package has been archived.
The intent of this feature is to make it easy to configure simple auditing
behavior. If you want more powerful auditing behavior, you can use Herald to
write more sophisticated rules.
+Ignored Attributes
+==================
+
+You can automatically exclude certain types of files, like generated files,
+with **Ignored Attributes**.
+
+When a package is marked as ignoring files with a particular attribute, and
+a file in a particular change has that attribute, the file will be ignored when
+computing ownership.
+
+(This feature is currently rough, only works for Differential revisions, and
+may not always compute the correct set of owning packages in some complex
+cases where it interacts with dominion rules.)
+
+
Files in Multiple Packages
==========================
Multiple packages may own the same file. For example, both the
"Android Application" and the "iOS Application" packages might own a path
like this, containing resources used by both:
/shared/assets/mobile/
If both packages own this directory, files in the directory are considered to
be part of both packages.
Packages do not need to have claims of equal specificity to own files. For
example, if you have a "Design Assets" package which owns this path:
/shared/assets/
...it will //also// own all of the files in the `mobile/` subdirectory. In this
configuration, these files are part of three packages: "iOS Application",
"Android Application", and "Design Assets".
(You can use an "exclude" rule if you want to make a different package with a
more specific claim the owner of a file or subdirectory. You can also change
the **Dominion** setting for a package to let it give up ownership of paths
owned by another package.)
diff --git a/src/docs/user/userguide/search.diviner b/src/docs/user/userguide/search.diviner
index d1c618212..f1b32e7dc 100644
--- a/src/docs/user/userguide/search.diviner
+++ b/src/docs/user/userguide/search.diviner
@@ -1,175 +1,184 @@
@title Search User Guide
@group userguide
Introduction to searching for documents in Phabricator.
Overview
========
Phabricator has two major ways to search for documents and objects (like tasks,
code reviews, users, wiki documents, and so on): **global search** and
**application search**.
**Global search** allows you to search across multiple document types at once,
but has fewer options for refining a search. It's a good general-purpose
search, and helpful if you're searching for a text string.
**Application search** allows you to search within an application (like
Maniphest) for documents of a specific type. Because application search is only
searching one type of object, it can provide more powerful options for
filtering, ordering, and displaying the results.
Both types of search share many of the same features. This document walks
through how to use search and how to take advantage of some of the advanced
options.
Global Search
=============
Global search allows you to search across multiple document types at once.
You can access global search by entering a search query in the main menu bar.
By default, global search queries search all document types: for example, they
will find matching tasks, commits, wiki documents, users, etc. You can use the
dropdown to the left of the search box to select a different search scope.
If you choose the **Current Application** scope, Phabricator will search for
open documents in the current application. For example, if you're in Maniphest
and run a search, you'll get matching tasks. If you're in Phriction and run a
search, you'll get matching wiki documents.
Some pages (like the 404 page) don't belong to an application, or belong to an
application which doesn't have any searchable documents. In these cases,
Phabricator will search all documents.
To quickly **jump to an object** like a task, enter the object's ID in the
global search box and search for it. For example, you can enter `T123` or
`D456` to quickly jump to the corresponding task or code review, or enter a Git
commit hash to jump to the corresponding commit. For a complete list of
supported commands, see @{article:Search User Guide: Shortcuts}.
After running a search, you can scroll up to add filters and refine the result
set. You can also select **Advanced Search** from the dropdown menu to jump
here immediately, or press return in the search box without entering a query.
This interface supports standard Phabricator search and filtering features,
like **saved queries** and **typeaheads**. See below for more details on using
these features.
Application Search
==================
Application search gives you a more powerful way to search one type of document,
like tasks. Most applications provide application search interfaces for the
documents or objects they let you create: these pages have queries in the left
menu, show objects or documents in the main content area, and have controls
for refining the results.
These interfaces support **saved queries** and **typeaheads**.
Saving and Sharing Queries
=============
If you have a query which you run often, you can save it for easy access.
To do this, click "Save Custom Query..." on the result screen. Choose a name
for your query and it will be saved in the left nav so you can run it again
with one click.
You can use "Edit Queries..." to reorder queries or remove saved queries you
don't use anymore.
If you drag a query to the top of the list, it will execute by default when
you load the relevant search interface. You can use this to make your default
view show the results you most often want.
You can share queries with other users by sending them the URL. This will run
the same query for them with all the parameters you've set (they may see
different results than you do, because they may not have the same permissions).
Typeaheads
==========
Typeaheads are text inputs which suggest options as you type. Typeaheads make
it easy to select users, projects, document types, and other kinds of objects
without typing their full names.
For example, if you want to find tasks that a specific user created, you can
use the "Authors:" filter in Maniphest. The filter uses a typeahead control
to let you enter authors who you want to search for.
To use a typeahead, enter the first few letters of the thing you want to
select. It will appear in a dropdown under your cursor, and you can select it
by clicking it (or using the arrow keys to highlight it, then pressing return).
If you aren't sure about the exact name of what you're looking for, click the
browse button ({nav icon=search}) to the right of the input. This will let you
browse through valid results for the control. You can filter the results from
within the browse dialog to narrow them down.
Some typeaheads support advanced selection functions which can let you build
more powerful queries. If a control supports functions, the "Browse" dialog
will show that advanced functions are available and give you a link to details
on which functions you can use.
For example, the `members()` function lets you automatically select all of the
members of a project. You could use this with the "Authors" filter to find
tasks created by anyone on a certain team.
Another useful function is the `viewer()` function, which works as though you'd
typed your own username when you run the query. However, if you send the query
to someone else, it will show results for //their// username when they run it.
This can be particularly useful when creating dashboard panels.
Fulltext Search
===============
Global search and some applications provide **fulltext search**. In
applications, this is a field called {nav Query}.
Fulltext search allows you to search the text content of objects and supports
some special syntax. These features are supported:
- Substring search with `~platypus`.
- Field search with `title:platypus`.
- Filtering out matches with `-platypus`.
- Quoted terms with `"platypus attorney"`.
+ - Matching entire fields with `=platypus`.
- Combining features with `title:~"platypus attorney"`.
See below for more detail.
**Substrings**: Normally, query terms are searched for as words, so searching
for `read` won't find documents which only contain the word `threaded`, even
though "read" is a substring of "threaded". With the substring operator, `~`,
you can search for substrings instead: the query `~read` will match documents
which contain that text anywhere, even in the middle of a word.
**Quoted Terms**: When you search for multiple terms, documents which match
each term will be returned, even if the terms are not adjacent in the document.
For example, the query `void star` will match a document titled `A star in the
void`, because it matches both `void` and `star`. To search for an exact
sequence of terms, quote them: `"void star"`. This query will only match
documents which use those terms as written.
**Stemming**: Searching for a term like `rearming` will find documents which
contain variations of the word, like `rearm`, `rearms`, and `rearmed`. To
search for an an exact word, quote the term: `"rearming"`.
**Field Search**: By default, query terms are searched for in the title, body,
and comments. If you only want to search for a term in titles, use `title:`.
For example, `title:platypus` only finds documents with that term in the
title. This can be combined with other operators, for example `title:~platypus`
or `title:"platypus attorney"`. These scopes are also supported:
- `title:...` searches titles.
- `body:...` searches bodies (descriptions or summaries).
- `core:...` searches titles and bodies, but not comments.
- `comments:...` searches only comments.
**Filtering Matches**: You can remove documents which match certain terms from
the result set with `-`. For example: `platypus -mammal`. Documents which match
negated terms will be filtered out of the result set.
+
+**Matching Entire Fields**: If you know the exact name of an object and want
+to find only that object, you can use the `=` operator. A query like
+`title:"warp drive"` will find a document titled "Warp Drive", but will also
+find documents with longer titles, like "Not a Warp Drive". The `=` operator
+requires that the entire field match the query exactly, so //only// documents
+exactly titled "Warp Drive" will be matched by the query (but note that the
+query is still case insensitive).
diff --git a/src/docs/user/userguide/webhooks.diviner b/src/docs/user/userguide/webhooks.diviner
index 51521b462..10d1f36da 100644
--- a/src/docs/user/userguide/webhooks.diviner
+++ b/src/docs/user/userguide/webhooks.diviner
@@ -1,212 +1,223 @@
@title User Guide: Webhooks
@group userguide
Guide to configuring webhooks.
Overview
========
If you'd like to react to events in Phabricator or publish them into external
systems, you can configure webhooks.
Configure webhooks in {nav Herald > Webhooks}. Users must have the
"Can Create Webhooks" permission to create new webhooks.
Triggering Hooks
================
Webhooks can be triggered in two ways:
- Set the hook mode to **Firehose**. In this mode, your hook will be called
for every event.
- Set the hook mode to **Enabled**, then write Herald rules which use the
**Call webhooks** action to choose when the hook is called. This allows
you to choose a narrower range of events to be notified about.
Testing Hooks
=============
To test a webhook, use {nav New Test Request} from the web interface.
You can also use the command-line tool, which supports a few additional
options:
```
phabricator/ $ ./bin/webhook call --id 42 --object D123
```
-You can use a tool like [[ https://requestb.in | RequestBin ]] to inspect
-the headers and payload for calls to hooks.
-
Verifying Requests
==================
When your webhook callback URI receives a request, it didn't necessarily come
from Phabricator. An attacker or mischievous user can normally call your hook
directly and pretend to be notifying you of an event.
To verify that the request is authentic, first retrieve the webhook key from
the web UI with {nav View HMAC Key}. This is a shared secret which will let you
verify that Phabricator originated a request.
When you receive a request, compute the SHA256 HMAC value of the request body
using the HMAC key as the key. The value should match the value in the
`X-Phabricator-Webhook-Signature` field.
To compute the SHA256 HMAC of a string in PHP, do this:
```lang=php
$signature = hash_hmac('sha256', $request_body, $hmac_key);
```
To compute the SHA256 HMAC of a string in Python, do this:
```lang=python
from subprocess import check_output
signature = check_output(
[
"php",
"-r",
"echo hash_hmac('sha256', $argv[1], $argv[2]);",
"--",
request_body,
hmac_key
])
```
Other languages often provide similar support.
If you somehow disclose the key by accident, use {nav Regenerate HMAC Key} to
throw it away and generate a new one.
Request Format
==============
Webhook callbacks are POST requests with a JSON payload in the body. The
payload looks like this:
```lang=json
{
"object": {
"type": "TASK",
"phid": "PHID-TASK-abcd..."
},
"triggers": [
{
"phid": "PHID-HRUL-abcd..."
}
],
"action": {
"test": false,
"silent": false,
"secure": false,
"epoch": 12345
},
"transactions": [
{
"phid": "PHID-XACT-TASK-abcd..."
}
]
}
```
The **object** map describes the object which was edited.
The **triggers** are a list of reasons why the hook was called. When the hook
is triggered by Herald rules, the specific rules which triggered the call will
be listed. For firehose rules, the rule itself will be listed as the trigger.
For test calls, the user making the request will be listed as a trigger.
The **action** map has metadata about the action:
- `test` This was a test call from the web UI or console.
- `silent` This is a silent edit which won't send mail or notifications in
Phabricator. If your hook is doing something like copying events into
a chatroom, it may want to respect this flag.
- `secure` Details about this object should only be transmitted over
secure channels. Your hook may want to respect this flag.
- `epoch` The epoch timestamp when the callback was queued.
The **transactions** list contains information about the actual changes which
triggered the callback.
Responding to Requests
======================
Although trivial hooks may not need any more information than this to act, the
information conveyed in the hook body is a minimum set of pointers to relevant
data and likely insufficient for more complex hooks.
Complex hooks should expect to react to receiving a request by making API
calls to Conduit to retrieve additional information about the object and
transactions.
Hooks that are interested in reading object state should generally make a call
to a method like `maniphest.search` or `differential.revision.search` using
the PHID from the `object` field to retrieve full details about the object
state.
Hooks that are interested in changes should generally make a call to
`transaction.search`, passing the transaction PHIDs as a constraint to retrieve
details about the transactions.
+For example, your call to `transaction.search` may look something like this:
+
+```lang=json
+{
+ "objectIdentifier": "PHID-XXXX-abcdef",
+ "constraints": {
+ "phids": [
+ "PHID-XACT-XXXX-11111111",
+ "PHID-XACT-XXXX-22222222"
+ ]
+ }
+}
+```
+
The `phid.query` method can also be used to retrieve generic information about
a list of objects.
Retries and Rate Limiting
=========================
Test requests are never retried: they execute exactly once.
Live requests are automatically retried. If your endpoint does not return a
HTTP 2XX response, the request will be retried regularly until it suceeds.
Retries will continue until the request succeeds or is garbage collected. By
default, this is after 7 days.
If a webhook is disabled, outstanding queued requests will be failed
permanently. Activity which occurs while it is disabled will never be sent to
the callback URI. (Disabling a hook does not "pause" it so that it can be
"resumed" later and pick back up where it left off in the event stream.)
If a webhook encounters a significant number of errors in a short period of
time, the webhook will be paused for a few minutes before additional requests
are made. The web UI shows a warning indicator when a hook is paused because of
errors.
Hook requests time out after 10 seconds. Consider offloading response handling
to some kind of worker queue if you expect to routinely require more than 10
seconds to respond to requests.
Hook callbacks are single-threaded: you will never receive more than one
simultaneous call to the same webhook from Phabricator. If you have a firehose
hook on an active install, it may be important to respond to requests quickly
to avoid accumulating a backlog.
Callbacks may be invoked out-of-order. You should not assume that the order
you receive requests in is chronological order. If your hook is order-dependent,
you can ignore the transactions in the callback and use `transaction.search` to
retrieve a consistent list of ordered changes to the object.
Callbacks may be delayed for an arbitrarily long amount of time, up to the
garbage collection limit. You should not assume that calls are real time. If
your hook is doing something time-sensitive, you can measure the delivery delay
by comparing the current time to the `epoch` value in the `action` field and
ignoring old actions or handling them in some special way.
Next Steps
==========
Continue by:
- learning more about Herald with @{article:Herald User Guide}; or
- interacting with the Conduit API with @{article:Conduit API Overview}.
diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php
index 5a32ef7c1..7dc55427c 100644
--- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php
+++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php
@@ -1,731 +1,738 @@
<?php
final class PhabricatorDatabaseRef
extends Phobject {
const STATUS_OKAY = 'okay';
const STATUS_FAIL = 'fail';
const STATUS_AUTH = 'auth';
const STATUS_REPLICATION_CLIENT = 'replication-client';
const REPLICATION_OKAY = 'okay';
const REPLICATION_MASTER_REPLICA = 'master-replica';
const REPLICATION_REPLICA_NONE = 'replica-none';
const REPLICATION_SLOW = 'replica-slow';
const REPLICATION_NOT_REPLICATING = 'not-replicating';
const KEY_HEALTH = 'cluster.db.health';
const KEY_REFS = 'cluster.db.refs';
const KEY_INDIVIDUAL = 'cluster.db.individual';
private $host;
private $port;
private $user;
private $pass;
private $disabled;
private $isMaster;
private $isIndividual;
private $connectionLatency;
private $connectionStatus;
private $connectionMessage;
+ private $connectionException;
private $replicaStatus;
private $replicaMessage;
private $replicaDelay;
private $healthRecord;
private $didFailToConnect;
private $isDefaultPartition;
private $applicationMap = array();
private $masterRef;
private $replicaRefs = array();
private $usePersistentConnections;
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setPort($port) {
$this->port = $port;
return $this;
}
public function getPort() {
return $this->port;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setPass(PhutilOpaqueEnvelope $pass) {
$this->pass = $pass;
return $this;
}
public function getPass() {
return $this->pass;
}
public function setIsMaster($is_master) {
$this->isMaster = $is_master;
return $this;
}
public function getIsMaster() {
return $this->isMaster;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
public function setConnectionLatency($connection_latency) {
$this->connectionLatency = $connection_latency;
return $this;
}
public function getConnectionLatency() {
return $this->connectionLatency;
}
public function setConnectionStatus($connection_status) {
$this->connectionStatus = $connection_status;
return $this;
}
public function getConnectionStatus() {
if ($this->connectionStatus === null) {
throw new PhutilInvalidStateException('queryAll');
}
return $this->connectionStatus;
}
public function setConnectionMessage($connection_message) {
$this->connectionMessage = $connection_message;
return $this;
}
public function getConnectionMessage() {
return $this->connectionMessage;
}
public function setReplicaStatus($replica_status) {
$this->replicaStatus = $replica_status;
return $this;
}
public function getReplicaStatus() {
return $this->replicaStatus;
}
public function setReplicaMessage($replica_message) {
$this->replicaMessage = $replica_message;
return $this;
}
public function getReplicaMessage() {
return $this->replicaMessage;
}
public function setReplicaDelay($replica_delay) {
$this->replicaDelay = $replica_delay;
return $this;
}
public function getReplicaDelay() {
return $this->replicaDelay;
}
public function setIsIndividual($is_individual) {
$this->isIndividual = $is_individual;
return $this;
}
public function getIsIndividual() {
return $this->isIndividual;
}
public function setIsDefaultPartition($is_default_partition) {
$this->isDefaultPartition = $is_default_partition;
return $this;
}
public function getIsDefaultPartition() {
return $this->isDefaultPartition;
}
public function setUsePersistentConnections($use_persistent_connections) {
$this->usePersistentConnections = $use_persistent_connections;
return $this;
}
public function getUsePersistentConnections() {
return $this->usePersistentConnections;
}
public function setApplicationMap(array $application_map) {
$this->applicationMap = $application_map;
return $this;
}
public function getApplicationMap() {
return $this->applicationMap;
}
public function getPartitionStateForCommit() {
$state = PhabricatorEnv::getEnvConfig('cluster.databases');
foreach ($state as $key => $value) {
// Don't store passwords, since we don't care if they differ and
// users may find it surprising.
unset($state[$key]['pass']);
}
return phutil_json_encode($state);
}
public function setMasterRef(PhabricatorDatabaseRef $master_ref) {
$this->masterRef = $master_ref;
return $this;
}
public function getMasterRef() {
return $this->masterRef;
}
public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) {
$this->replicaRefs[] = $replica_ref;
return $this;
}
public function getReplicaRefs() {
return $this->replicaRefs;
}
public function getRefKey() {
$host = $this->getHost();
$port = $this->getPort();
if (strlen($port)) {
return "{$host}:{$port}";
}
return $host;
}
public static function getConnectionStatusMap() {
return array(
self::STATUS_OKAY => array(
'icon' => 'fa-exchange',
'color' => 'green',
'label' => pht('Okay'),
),
self::STATUS_FAIL => array(
'icon' => 'fa-times',
'color' => 'red',
'label' => pht('Failed'),
),
self::STATUS_AUTH => array(
'icon' => 'fa-key',
'color' => 'red',
'label' => pht('Invalid Credentials'),
),
self::STATUS_REPLICATION_CLIENT => array(
'icon' => 'fa-eye-slash',
'color' => 'yellow',
'label' => pht('Missing Permission'),
),
);
}
public static function getReplicaStatusMap() {
return array(
self::REPLICATION_OKAY => array(
'icon' => 'fa-download',
'color' => 'green',
'label' => pht('Okay'),
),
self::REPLICATION_MASTER_REPLICA => array(
'icon' => 'fa-database',
'color' => 'red',
'label' => pht('Replicating Master'),
),
self::REPLICATION_REPLICA_NONE => array(
'icon' => 'fa-download',
'color' => 'red',
'label' => pht('Not A Replica'),
),
self::REPLICATION_SLOW => array(
'icon' => 'fa-hourglass',
'color' => 'red',
'label' => pht('Slow Replication'),
),
self::REPLICATION_NOT_REPLICATING => array(
'icon' => 'fa-exclamation-triangle',
'color' => 'red',
'label' => pht('Not Replicating'),
),
);
}
public static function getClusterRefs() {
$cache = PhabricatorCaches::getRequestCache();
$refs = $cache->getKey(self::KEY_REFS);
if (!$refs) {
$refs = self::newRefs();
$cache->setKey(self::KEY_REFS, $refs);
}
return $refs;
}
public static function getLiveIndividualRef() {
$cache = PhabricatorCaches::getRequestCache();
$ref = $cache->getKey(self::KEY_INDIVIDUAL);
if (!$ref) {
$ref = self::newIndividualRef();
$cache->setKey(self::KEY_INDIVIDUAL, $ref);
}
return $ref;
}
public static function newRefs() {
$default_port = PhabricatorEnv::getEnvConfig('mysql.port');
$default_port = nonempty($default_port, 3306);
$default_user = PhabricatorEnv::getEnvConfig('mysql.user');
$default_pass = PhabricatorEnv::getEnvConfig('mysql.pass');
$default_pass = new PhutilOpaqueEnvelope($default_pass);
$config = PhabricatorEnv::getEnvConfig('cluster.databases');
return id(new PhabricatorDatabaseRefParser())
->setDefaultPort($default_port)
->setDefaultUser($default_user)
->setDefaultPass($default_pass)
->newRefs($config);
}
public static function queryAll() {
$refs = self::getActiveDatabaseRefs();
return self::queryRefs($refs);
}
private static function queryRefs(array $refs) {
foreach ($refs as $ref) {
$conn = $ref->newManagementConnection();
$t_start = microtime(true);
$replica_status = false;
try {
$replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS');
$ref->setConnectionStatus(self::STATUS_OKAY);
} catch (AphrontAccessDeniedQueryException $ex) {
$ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT);
$ref->setConnectionMessage(
pht(
'No permission to run "SHOW SLAVE STATUS". Grant this user '.
'"REPLICATION CLIENT" permission to allow Phabricator to '.
'monitor replica health.'));
} catch (AphrontInvalidCredentialsQueryException $ex) {
$ref->setConnectionStatus(self::STATUS_AUTH);
$ref->setConnectionMessage($ex->getMessage());
} catch (AphrontQueryException $ex) {
$ref->setConnectionStatus(self::STATUS_FAIL);
$class = get_class($ex);
$message = $ex->getMessage();
$ref->setConnectionMessage(
pht(
'%s: %s',
get_class($ex),
$ex->getMessage()));
}
$t_end = microtime(true);
$ref->setConnectionLatency($t_end - $t_start);
if ($replica_status !== false) {
$is_replica = (bool)$replica_status;
if ($ref->getIsMaster() && $is_replica) {
$ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA);
$ref->setReplicaMessage(
pht(
'This host has a "master" role, but is replicating data from '.
'another host ("%s")!',
idx($replica_status, 'Master_Host')));
} else if (!$ref->getIsMaster() && !$is_replica) {
$ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE);
$ref->setReplicaMessage(
pht(
'This host has a "replica" role, but is not replicating data '.
'from a master (no output from "SHOW SLAVE STATUS").'));
} else {
$ref->setReplicaStatus(self::REPLICATION_OKAY);
}
if ($is_replica) {
$latency = idx($replica_status, 'Seconds_Behind_Master');
if (!strlen($latency)) {
$ref->setReplicaStatus(self::REPLICATION_NOT_REPLICATING);
} else {
$latency = (int)$latency;
$ref->setReplicaDelay($latency);
if ($latency > 30) {
$ref->setReplicaStatus(self::REPLICATION_SLOW);
$ref->setReplicaMessage(
pht(
'This replica is lagging far behind the master. Data is at '.
'risk!'));
}
}
}
}
}
return $refs;
}
public function newManagementConnection() {
return $this->newConnection(
array(
'retries' => 0,
'timeout' => 2,
));
}
public function newApplicationConnection($database) {
return $this->newConnection(
array(
'database' => $database,
));
}
public function isSevered() {
// If we only have an individual database, never sever our connection to
// it, at least for now. It's possible that using the same severing rules
// might eventually make sense to help alleviate load-related failures,
// but we should wait for all the cluster stuff to stabilize first.
if ($this->getIsIndividual()) {
return false;
}
if ($this->didFailToConnect) {
return true;
}
$record = $this->getHealthRecord();
$is_healthy = $record->getIsHealthy();
if (!$is_healthy) {
return true;
}
return false;
}
public function isReachable(AphrontDatabaseConnection $connection) {
$record = $this->getHealthRecord();
$should_check = $record->getShouldCheck();
if ($this->isSevered() && !$should_check) {
return false;
}
+ $this->connectionException = null;
try {
$connection->openConnection();
$reachable = true;
} catch (AphrontSchemaQueryException $ex) {
// We get one of these if the database we're trying to select does not
// exist. In this case, just re-throw the exception. This is expected
// during first-time setup, when databases like "config" will not exist
// yet.
throw $ex;
} catch (Exception $ex) {
+ $this->connectionException = $ex;
$reachable = false;
}
if ($should_check) {
$record->didHealthCheck($reachable);
}
if (!$reachable) {
$this->didFailToConnect = true;
}
return $reachable;
}
public function checkHealth() {
$health = $this->getHealthRecord();
$should_check = $health->getShouldCheck();
if ($should_check) {
// This does an implicit health update.
$connection = $this->newManagementConnection();
$this->isReachable($connection);
}
return $this;
}
private function getHealthRecordCacheKey() {
$host = $this->getHost();
$port = $this->getPort();
$key = self::KEY_HEALTH;
return "{$key}({$host}, {$port})";
}
public function getHealthRecord() {
if (!$this->healthRecord) {
$this->healthRecord = new PhabricatorClusterServiceHealthRecord(
$this->getHealthRecordCacheKey());
}
return $this->healthRecord;
}
+ public function getConnectionException() {
+ return $this->connectionException;
+ }
+
public static function getActiveDatabaseRefs() {
$refs = array();
foreach (self::getMasterDatabaseRefs() as $ref) {
$refs[] = $ref;
}
foreach (self::getReplicaDatabaseRefs() as $ref) {
$refs[] = $ref;
}
return $refs;
}
public static function getAllMasterDatabaseRefs() {
$refs = self::getClusterRefs();
if (!$refs) {
return array(self::getLiveIndividualRef());
}
$masters = array();
foreach ($refs as $ref) {
if ($ref->getIsMaster()) {
$masters[] = $ref;
}
}
return $masters;
}
public static function getMasterDatabaseRefs() {
$refs = self::getAllMasterDatabaseRefs();
return self::getEnabledRefs($refs);
}
public function isApplicationHost($database) {
return isset($this->applicationMap[$database]);
}
public function loadRawMySQLConfigValue($key) {
$conn = $this->newManagementConnection();
try {
$value = queryfx_one($conn, 'SELECT @@%Q', $key);
$value = $value['@@'.$key];
} catch (AphrontQueryException $ex) {
$value = null;
}
return $value;
}
public static function getMasterDatabaseRefForApplication($application) {
$masters = self::getMasterDatabaseRefs();
$application_master = null;
$default_master = null;
foreach ($masters as $master) {
if ($master->isApplicationHost($application)) {
$application_master = $master;
break;
}
if ($master->getIsDefaultPartition()) {
$default_master = $master;
}
}
if ($application_master) {
$masters = array($application_master);
} else if ($default_master) {
$masters = array($default_master);
} else {
$masters = array();
}
$masters = self::getEnabledRefs($masters);
$master = head($masters);
return $master;
}
public static function newIndividualRef() {
$default_user = PhabricatorEnv::getEnvConfig('mysql.user');
$default_pass = new PhutilOpaqueEnvelope(
PhabricatorEnv::getEnvConfig('mysql.pass'));
$default_host = PhabricatorEnv::getEnvConfig('mysql.host');
$default_port = PhabricatorEnv::getEnvConfig('mysql.port');
return id(new self())
->setUser($default_user)
->setPass($default_pass)
->setHost($default_host)
->setPort($default_port)
->setIsIndividual(true)
->setIsMaster(true)
->setIsDefaultPartition(true)
->setUsePersistentConnections(false);
}
public static function getAllReplicaDatabaseRefs() {
$refs = self::getClusterRefs();
if (!$refs) {
return array();
}
$replicas = array();
foreach ($refs as $ref) {
if ($ref->getIsMaster()) {
continue;
}
$replicas[] = $ref;
}
return $replicas;
}
public static function getReplicaDatabaseRefs() {
$refs = self::getAllReplicaDatabaseRefs();
return self::getEnabledRefs($refs);
}
private static function getEnabledRefs(array $refs) {
foreach ($refs as $key => $ref) {
if ($ref->getDisabled()) {
unset($refs[$key]);
}
}
return $refs;
}
public static function getReplicaDatabaseRefForApplication($application) {
$replicas = self::getReplicaDatabaseRefs();
$application_replicas = array();
$default_replicas = array();
foreach ($replicas as $replica) {
$master = $replica->getMasterRef();
if ($master->isApplicationHost($application)) {
$application_replicas[] = $replica;
}
if ($master->getIsDefaultPartition()) {
$default_replicas[] = $replica;
}
}
if ($application_replicas) {
$replicas = $application_replicas;
} else {
$replicas = $default_replicas;
}
$replicas = self::getEnabledRefs($replicas);
// TODO: We may have multiple replicas to choose from, and could make
// more of an effort to pick the "best" one here instead of always
// picking the first one. Once we've picked one, we should try to use
// the same replica for the rest of the request, though.
return head($replicas);
}
private function newConnection(array $options) {
// If we believe the database is unhealthy, don't spend as much time
// trying to connect to it, since it's likely to continue to fail and
// hammering it can only make the problem worse.
$record = $this->getHealthRecord();
if ($record->getIsHealthy()) {
$default_retries = 3;
$default_timeout = 10;
} else {
$default_retries = 0;
$default_timeout = 2;
}
$spec = $options + array(
'user' => $this->getUser(),
'pass' => $this->getPass(),
'host' => $this->getHost(),
'port' => $this->getPort(),
'database' => null,
'retries' => $default_retries,
'timeout' => $default_timeout,
'persistent' => $this->getUsePersistentConnections(),
);
$is_cli = (php_sapi_name() == 'cli');
$use_persistent = false;
if (!empty($spec['persistent']) && !$is_cli) {
$use_persistent = true;
}
unset($spec['persistent']);
$connection = self::newRawConnection($spec);
// If configured, use persistent connections. See T11672 for details.
if ($use_persistent) {
$connection->setPersistent($use_persistent);
}
// Unless this is a script running from the CLI, prevent any query from
// running for more than 30 seconds. See T10849 for details.
if (!$is_cli) {
$connection->setQueryTimeout(30);
}
return $connection;
}
public static function newRawConnection(array $options) {
if (extension_loaded('mysqli')) {
return new AphrontMySQLiDatabaseConnection($options);
} else {
return new AphrontMySQLDatabaseConnection($options);
}
}
}
diff --git a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
index 03f30506b..387014289 100644
--- a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
+++ b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
@@ -1,102 +1,104 @@
<?php
final class PhabricatorClusterMailersConfigType
extends PhabricatorJSONConfigType {
const TYPEKEY = 'cluster.mailers';
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
if ($value === null) {
return;
}
if (!is_array($value)) {
throw $this->newException(
pht(
'Mailer cluster configuration is not valid: it should be a list '.
'of mailer configurations.'));
}
foreach ($value as $index => $spec) {
if (!is_array($spec)) {
throw $this->newException(
pht(
'Mailer cluster configuration is not valid: each entry in the '.
'list must be a dictionary describing a mailer, but the value '.
'with index "%s" is not a dictionary.',
$index));
}
}
$adapters = PhabricatorMailImplementationAdapter::getAllAdapters();
$map = array();
foreach ($value as $index => $spec) {
try {
PhutilTypeSpec::checkMap(
$spec,
array(
'key' => 'string',
'type' => 'string',
'priority' => 'optional int',
'options' => 'optional wild',
+ 'inbound' => 'optional bool',
+ 'outbound' => 'optional bool',
));
} catch (Exception $ex) {
throw $this->newException(
pht(
'Mailer configuration has an invalid mailer specification '.
'(at index "%s"): %s.',
$index,
$ex->getMessage()));
}
$key = $spec['key'];
if (isset($map[$key])) {
throw $this->newException(
pht(
'Mailer configuration is invalid: multiple mailers have the same '.
'key ("%s"). Each mailer must have a unique key.',
$key));
}
$map[$key] = true;
$priority = idx($spec, 'priority');
if ($priority !== null && $priority <= 0) {
throw $this->newException(
pht(
'Mailer configuration ("%s") is invalid: priority must be '.
'greater than 0.',
$key));
}
$type = $spec['type'];
if (!isset($adapters[$type])) {
throw $this->newException(
pht(
'Mailer configuration ("%s") is invalid: mailer type ("%s") is '.
'unknown. Supported mailer types are: %s.',
$key,
$type,
implode(', ', array_keys($adapters))));
}
$options = idx($spec, 'options', array());
try {
$defaults = $adapters[$type]->newDefaultOptions();
$options = $options + $defaults;
id(clone $adapters[$type])->setOptions($options);
} catch (Exception $ex) {
throw $this->newException(
pht(
'Mailer configuration ("%s") specifies invalid options for '.
'mailer: %s',
$key,
$ex->getMessage()));
}
}
}
}
diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php
index d7f1a9dd3..e6572672c 100644
--- a/src/infrastructure/diff/PhabricatorInlineCommentController.php
+++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php
@@ -1,398 +1,407 @@
<?php
abstract class PhabricatorInlineCommentController
extends PhabricatorController {
abstract protected function createComment();
abstract protected function loadComment($id);
abstract protected function loadCommentForEdit($id);
abstract protected function loadCommentForDone($id);
abstract protected function loadCommentByPHID($phid);
abstract protected function loadObjectOwnerPHID(
PhabricatorInlineCommentInterface $inline);
abstract protected function deleteComment(
PhabricatorInlineCommentInterface $inline);
abstract protected function undeleteComment(
PhabricatorInlineCommentInterface $inline);
abstract protected function saveComment(
PhabricatorInlineCommentInterface $inline);
protected function hideComments(array $ids) {
throw new PhutilMethodNotImplementedException();
}
protected function showComments(array $ids) {
throw new PhutilMethodNotImplementedException();
}
private $changesetID;
private $isNewFile;
private $isOnRight;
private $lineNumber;
private $lineLength;
private $commentText;
private $operation;
private $commentID;
private $renderer;
private $replyToCommentPHID;
public function getCommentID() {
return $this->commentID;
}
public function getOperation() {
return $this->operation;
}
public function getCommentText() {
return $this->commentText;
}
public function getLineLength() {
return $this->lineLength;
}
public function getLineNumber() {
return $this->lineNumber;
}
public function getIsOnRight() {
return $this->isOnRight;
}
public function getChangesetID() {
return $this->changesetID;
}
public function getIsNewFile() {
return $this->isNewFile;
}
public function setRenderer($renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
return $this->renderer;
}
public function setReplyToCommentPHID($phid) {
$this->replyToCommentPHID = $phid;
return $this;
}
public function getReplyToCommentPHID() {
return $this->replyToCommentPHID;
}
public function processRequest() {
$request = $this->getRequest();
- $user = $request->getUser();
+ $viewer = $this->getViewer();
$this->readRequestParameters();
$op = $this->getOperation();
switch ($op) {
case 'hide':
case 'show':
if (!$request->validateCSRF()) {
return new Aphront404Response();
}
$ids = $request->getStrList('ids');
if ($ids) {
if ($op == 'hide') {
$this->hideComments($ids);
} else {
$this->showComments($ids);
}
}
return id(new AphrontAjaxResponse())->setContent(array());
case 'done':
if (!$request->validateCSRF()) {
return new Aphront404Response();
}
$inline = $this->loadCommentForDone($this->getCommentID());
$is_draft_state = false;
$is_checked = false;
switch ($inline->getFixedState()) {
case PhabricatorInlineCommentInterface::STATE_DRAFT:
$next_state = PhabricatorInlineCommentInterface::STATE_UNDONE;
break;
case PhabricatorInlineCommentInterface::STATE_UNDRAFT:
$next_state = PhabricatorInlineCommentInterface::STATE_DONE;
$is_checked = true;
break;
case PhabricatorInlineCommentInterface::STATE_DONE:
$next_state = PhabricatorInlineCommentInterface::STATE_UNDRAFT;
$is_draft_state = true;
break;
default:
case PhabricatorInlineCommentInterface::STATE_UNDONE:
$next_state = PhabricatorInlineCommentInterface::STATE_DRAFT;
$is_draft_state = true;
$is_checked = true;
break;
}
$inline->setFixedState($next_state)->save();
return id(new AphrontAjaxResponse())
->setContent(
array(
'isChecked' => $is_checked,
'draftState' => $is_draft_state,
));
case 'delete':
case 'undelete':
case 'refdelete':
if (!$request->validateCSRF()) {
return new Aphront404Response();
}
// NOTE: For normal deletes, we just process the delete immediately
// and show an "Undo" action. For deletes by reference from the
// preview ("refdelete"), we prompt first (because the "Undo" may
// not draw, or may not be easy to locate).
if ($op == 'refdelete') {
if (!$request->isFormPost()) {
return $this->newDialog()
->setTitle(pht('Really delete comment?'))
->addHiddenInput('id', $this->getCommentID())
->addHiddenInput('op', $op)
->appendParagraph(pht('Delete this inline comment?'))
->addCancelButton('#')
->addSubmitButton(pht('Delete'));
}
}
$is_delete = ($op == 'delete' || $op == 'refdelete');
$inline = $this->loadCommentForEdit($this->getCommentID());
if ($is_delete) {
$this->deleteComment($inline);
} else {
$this->undeleteComment($inline);
}
return $this->buildEmptyResponse();
case 'edit':
$inline = $this->loadCommentForEdit($this->getCommentID());
$text = $this->getCommentText();
if ($request->isFormPost()) {
if (strlen($text)) {
$inline->setContent($text);
$this->saveComment($inline);
return $this->buildRenderedCommentResponse(
$inline,
$this->getIsOnRight());
} else {
$this->deleteComment($inline);
return $this->buildEmptyResponse();
}
}
$edit_dialog = $this->buildEditDialog();
$edit_dialog->setTitle(pht('Edit Inline Comment'));
$edit_dialog->addHiddenInput('id', $this->getCommentID());
$edit_dialog->addHiddenInput('op', 'edit');
$edit_dialog->appendChild(
$this->renderTextArea(
nonempty($text, $inline->getContent())));
$view = $this->buildScaffoldForView($edit_dialog);
return id(new AphrontAjaxResponse())
->setContent($view->render());
case 'create':
$text = $this->getCommentText();
if (!$request->isFormPost() || !strlen($text)) {
return $this->buildEmptyResponse();
}
$inline = $this->createComment()
->setChangesetID($this->getChangesetID())
- ->setAuthorPHID($user->getPHID())
+ ->setAuthorPHID($viewer->getPHID())
->setLineNumber($this->getLineNumber())
->setLineLength($this->getLineLength())
->setIsNewFile($this->getIsNewFile())
->setContent($text);
if ($this->getReplyToCommentPHID()) {
$inline->setReplyToCommentPHID($this->getReplyToCommentPHID());
}
+ // If you own this object, mark your own inlines as "Done" by default.
+ $owner_phid = $this->loadObjectOwnerPHID($inline);
+ if ($owner_phid) {
+ if ($viewer->getPHID() == $owner_phid) {
+ $fixed_state = PhabricatorInlineCommentInterface::STATE_DRAFT;
+ $inline->setFixedState($fixed_state);
+ }
+ }
+
$this->saveComment($inline);
return $this->buildRenderedCommentResponse(
$inline,
$this->getIsOnRight());
case 'reply':
default:
$edit_dialog = $this->buildEditDialog();
if ($this->getOperation() == 'reply') {
$edit_dialog->setTitle(pht('Reply to Inline Comment'));
} else {
$edit_dialog->setTitle(pht('New Inline Comment'));
}
// NOTE: We read the values from the client (the display values), not
// the values from the database (the original values) when replying.
// In particular, when replying to a ghost comment which was moved
// across diffs and then moved backward to the most recent visible
// line, we want to reply on the display line (which exists), not on
// the comment's original line (which may not exist in this changeset).
$is_new = $this->getIsNewFile();
$number = $this->getLineNumber();
$length = $this->getLineLength();
$edit_dialog->addHiddenInput('op', 'create');
$edit_dialog->addHiddenInput('is_new', $is_new);
$edit_dialog->addHiddenInput('number', $number);
$edit_dialog->addHiddenInput('length', $length);
$text_area = $this->renderTextArea($this->getCommentText());
$edit_dialog->appendChild($text_area);
$view = $this->buildScaffoldForView($edit_dialog);
return id(new AphrontAjaxResponse())
->setContent($view->render());
}
}
private function readRequestParameters() {
$request = $this->getRequest();
// NOTE: This isn't necessarily a DifferentialChangeset ID, just an
// application identifier for the changeset. In Diffusion, it's a Path ID.
$this->changesetID = $request->getInt('changesetID');
$this->isNewFile = (int)$request->getBool('is_new');
$this->isOnRight = $request->getBool('on_right');
$this->lineNumber = $request->getInt('number');
$this->lineLength = $request->getInt('length');
$this->commentText = $request->getStr('text');
$this->commentID = $request->getInt('id');
$this->operation = $request->getStr('op');
$this->renderer = $request->getStr('renderer');
$this->replyToCommentPHID = $request->getStr('replyToCommentPHID');
if ($this->getReplyToCommentPHID()) {
$reply_phid = $this->getReplyToCommentPHID();
$reply_comment = $this->loadCommentByPHID($reply_phid);
if (!$reply_comment) {
throw new Exception(
pht('Failed to load comment "%s".', $reply_phid));
}
// When replying, force the new comment into the same location as the
// old comment. If we don't do this, replying to a ghost comment from
// diff A while viewing diff B can end up placing the two comments in
// different places while viewing diff C, because the porting algorithm
// makes a different decision. Forcing the comments to bind to the same
// place makes sure they stick together no matter which diff is being
// viewed. See T10562 for discussion.
$this->changesetID = $reply_comment->getChangesetID();
$this->isNewFile = $reply_comment->getIsNewFile();
$this->lineNumber = $reply_comment->getLineNumber();
$this->lineLength = $reply_comment->getLineLength();
}
}
private function buildEditDialog() {
$request = $this->getRequest();
- $user = $request->getUser();
+ $viewer = $this->getViewer();
$edit_dialog = id(new PHUIDiffInlineCommentEditView())
- ->setUser($user)
+ ->setUser($viewer)
->setSubmitURI($request->getRequestURI())
->setIsOnRight($this->getIsOnRight())
->setIsNewFile($this->getIsNewFile())
->setNumber($this->getLineNumber())
->setLength($this->getLineLength())
->setRenderer($this->getRenderer())
->setReplyToCommentPHID($this->getReplyToCommentPHID())
->setChangesetID($this->getChangesetID());
return $edit_dialog;
}
private function buildEmptyResponse() {
return id(new AphrontAjaxResponse())
->setContent(
array(
'markup' => '',
));
}
private function buildRenderedCommentResponse(
PhabricatorInlineCommentInterface $inline,
$on_right) {
$request = $this->getRequest();
- $user = $request->getUser();
+ $viewer = $this->getViewer();
$engine = new PhabricatorMarkupEngine();
- $engine->setViewer($user);
+ $engine->setViewer($viewer);
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
$engine->process();
- $phids = array($user->getPHID());
+ $phids = array($viewer->getPHID());
$handles = $this->loadViewerHandles($phids);
$object_owner_phid = $this->loadObjectOwnerPHID($inline);
$view = id(new PHUIDiffInlineCommentDetailView())
- ->setUser($user)
+ ->setUser($viewer)
->setInlineComment($inline)
->setIsOnRight($on_right)
->setMarkupEngine($engine)
->setHandles($handles)
->setEditable(true)
->setCanMarkDone(false)
->setObjectOwnerPHID($object_owner_phid);
$view = $this->buildScaffoldForView($view);
return id(new AphrontAjaxResponse())
->setContent(
array(
'inlineCommentID' => $inline->getID(),
'markup' => $view->render(),
));
}
private function renderTextArea($text) {
return id(new PhabricatorRemarkupControl())
- ->setUser($this->getRequest()->getUser())
+ ->setViewer($this->getViewer())
->setSigil('differential-inline-comment-edit-textarea')
->setName('text')
->setValue($text)
->setDisableFullScreen(true);
}
private function buildScaffoldForView(PHUIDiffInlineCommentView $view) {
$renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey(
$this->getRenderer());
$view = $renderer->getRowScaffoldForInline($view);
return id(new PHUIDiffInlineCommentTableScaffold())
->addRowScaffold($view);
}
}
diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
index 27a489a70..5336af56d 100644
--- a/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
+++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
@@ -1,503 +1,524 @@
<?php
final class PHUIDiffInlineCommentDetailView
extends PHUIDiffInlineCommentView {
private $inlineComment;
private $handles;
private $markupEngine;
private $editable;
private $preview;
private $allowReply;
private $renderer;
private $canMarkDone;
private $objectOwnerPHID;
public function setInlineComment(PhabricatorInlineCommentInterface $comment) {
$this->inlineComment = $comment;
return $this;
}
public function isHidden() {
return $this->inlineComment->isHidden();
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setAllowReply($allow_reply) {
$this->allowReply = $allow_reply;
return $this;
}
public function setRenderer($renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
return $this->renderer;
}
public function setCanMarkDone($can_mark_done) {
$this->canMarkDone = $can_mark_done;
return $this;
}
public function getCanMarkDone() {
return $this->canMarkDone;
}
public function setObjectOwnerPHID($phid) {
$this->objectOwnerPHID = $phid;
return $this;
}
public function getObjectOwnerPHID() {
return $this->objectOwnerPHID;
}
public function getAnchorName() {
$inline = $this->inlineComment;
if ($inline->getID()) {
return 'inline-'.$inline->getID();
}
return null;
}
public function getScaffoldCellID() {
$anchor = $this->getAnchorName();
if ($anchor) {
return 'anchor-'.$anchor;
}
return null;
}
public function render() {
require_celerity_resource('phui-inline-comment-view-css');
$inline = $this->inlineComment;
$classes = array(
'differential-inline-comment',
);
$is_fixed = false;
switch ($inline->getFixedState()) {
case PhabricatorInlineCommentInterface::STATE_DONE:
case PhabricatorInlineCommentInterface::STATE_DRAFT:
$is_fixed = true;
break;
}
$is_draft_done = false;
switch ($inline->getFixedState()) {
case PhabricatorInlineCommentInterface::STATE_DRAFT:
case PhabricatorInlineCommentInterface::STATE_UNDRAFT:
$is_draft_done = true;
break;
}
$is_synthetic = false;
if ($inline->getSyntheticAuthor()) {
$is_synthetic = true;
}
$metadata = array(
'id' => $inline->getID(),
'phid' => $inline->getPHID(),
'changesetID' => $inline->getChangesetID(),
'number' => $inline->getLineNumber(),
'length' => $inline->getLineLength(),
'isNewFile' => (bool)$inline->getIsNewFile(),
'on_right' => $this->getIsOnRight(),
'original' => $inline->getContent(),
'replyToCommentPHID' => $inline->getReplyToCommentPHID(),
'isDraft' => $inline->isDraft(),
'isFixed' => $is_fixed,
'isGhost' => $inline->getIsGhost(),
'isSynthetic' => $is_synthetic,
'isDraftDone' => $is_draft_done,
);
$sigil = 'differential-inline-comment';
if ($this->preview) {
$sigil = $sigil.' differential-inline-comment-preview';
}
$classes = array(
'differential-inline-comment',
);
$content = $inline->getContent();
$handles = $this->handles;
$links = array();
$draft_text = null;
if (!$is_synthetic) {
// This display is controlled by CSS
$draft_text = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setName(pht('Unsubmitted'))
->setSlimShady(true)
->setColor(PHUITagView::COLOR_RED)
->addClass('mml inline-draft-text');
}
$ghost_tag = null;
$ghost = $inline->getIsGhost();
$ghost_id = null;
if ($ghost) {
if ($ghost['new']) {
$ghosticon = 'fa-fast-forward';
$reason = pht('View on forward revision');
} else {
$ghosticon = 'fa-fast-backward';
$reason = pht('View on previous revision');
}
$ghost_icon = id(new PHUIIconView())
->setIcon($ghosticon)
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $reason,
'size' => 300,
));
$ghost_tag = phutil_tag(
'a',
array(
'class' => 'ghost-icon',
'href' => $ghost['href'],
'target' => '_blank',
),
$ghost_icon);
$classes[] = 'inline-comment-ghost';
}
// I think this is unused
if ($inline->getHasReplies()) {
$classes[] = 'inline-comment-has-reply';
}
if ($inline->getReplyToCommentPHID()) {
$classes[] = 'inline-comment-is-reply';
}
$viewer_phid = $this->getUser()->getPHID();
$owner_phid = $this->getObjectOwnerPHID();
if ($viewer_phid) {
if ($viewer_phid == $owner_phid) {
$classes[] = 'viewer-is-object-owner';
}
}
$anchor_name = $this->getAnchorName();
$action_buttons = array();
$can_reply =
(!$this->editable) &&
(!$this->preview) &&
($this->allowReply) &&
// NOTE: No product reason why you can't reply to synthetic comments,
// but the reply mechanism currently sends the inline comment ID to the
// server, not file/line information, and synthetic comments don't have
// an inline comment ID.
(!$is_synthetic);
if ($can_reply) {
$action_buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-reply')
->setTooltip(pht('Reply'))
->addSigil('differential-inline-reply')
- ->setMustCapture(true);
+ ->setMustCapture(true)
+ ->setAuralLabel(pht('Reply'));
}
if ($this->editable && !$this->preview) {
$action_buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-pencil')
->setTooltip(pht('Edit'))
->addSigil('differential-inline-edit')
- ->setMustCapture(true);
+ ->setMustCapture(true)
+ ->setAuralLabel(pht('Edit'));
$action_buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-trash-o')
->setTooltip(pht('Delete'))
->addSigil('differential-inline-delete')
- ->setMustCapture(true);
+ ->setMustCapture(true)
+ ->setAuralLabel(pht('Delete'));
} else if ($this->preview) {
$links[] = javelin_tag(
'a',
array(
'class' => 'inline-button-divider pml msl',
'meta' => array(
'anchor' => $anchor_name,
),
'sigil' => 'differential-inline-preview-jump',
),
pht('View'));
$action_buttons[] = id(new PHUIButtonView())
->setTag('a')
->setTooltip(pht('Delete'))
->setIcon('fa-trash-o')
->addSigil('differential-inline-delete')
- ->setMustCapture(true);
+ ->setMustCapture(true)
+ ->setAuralLabel(pht('Delete'));
+ }
+
+ if (!$this->preview && $this->canHide()) {
+ $action_buttons[] = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setTooltip(pht('Collapse'))
+ ->setIcon('fa-times')
+ ->addSigil('hide-inline')
+ ->setMustCapture(true)
+ ->setAuralLabel(pht('Collapse'));
}
if (!$this->preview && $this->canHide()) {
$action_buttons[] = id(new PHUIButtonView())
->setTag('a')
->setTooltip(pht('Collapse'))
->setIcon('fa-times')
->addSigil('hide-inline')
->setMustCapture(true);
}
$done_button = null;
+ $mark_done = $this->getCanMarkDone();
+
+ // Allow users to mark their own draft inlines as "Done".
+ if ($viewer_phid == $inline->getAuthorPHID()) {
+ if ($inline->isDraft()) {
+ $mark_done = true;
+ }
+ }
+
if (!$is_synthetic) {
$draft_state = false;
switch ($inline->getFixedState()) {
case PhabricatorInlineCommentInterface::STATE_DRAFT:
- $is_done = ($this->getCanMarkDone());
+ $is_done = $mark_done;
$draft_state = true;
break;
case PhabricatorInlineCommentInterface::STATE_UNDRAFT:
- $is_done = !($this->getCanMarkDone());
+ $is_done = !$mark_done;
$draft_state = true;
break;
case PhabricatorInlineCommentInterface::STATE_DONE:
$is_done = true;
break;
default:
case PhabricatorInlineCommentInterface::STATE_UNDONE:
$is_done = false;
break;
}
// If you don't have permission to mark the comment as "Done", you also
// can not see the draft state.
- if (!$this->getCanMarkDone()) {
+ if (!$mark_done) {
$draft_state = false;
}
if ($is_done) {
$classes[] = 'inline-is-done';
}
if ($draft_state) {
$classes[] = 'inline-state-is-draft';
}
- if ($this->getCanMarkDone()) {
+ if ($mark_done && !$this->preview) {
$done_input = javelin_tag(
'input',
array(
'type' => 'checkbox',
'checked' => ($is_done ? 'checked' : null),
- 'disabled' => ($this->getCanMarkDone() ? null : 'disabled'),
'class' => 'differential-inline-done',
'sigil' => 'differential-inline-done',
));
$done_button = phutil_tag(
'label',
array(
- 'class' => 'differential-inline-done-label '.
- ($this->getCanMarkDone() ? null : 'done-is-disabled'),
+ 'class' => 'differential-inline-done-label ',
),
array(
$done_input,
pht('Done'),
));
} else {
if ($is_done) {
$icon = id(new PHUIIconView())->setIcon('fa-check sky msr');
$label = pht('Done');
$class = 'button-done';
} else {
$icon = null;
$label = pht('Not Done');
$class = 'button-not-done';
}
$done_button = phutil_tag(
'div',
array(
'class' => 'done-label '.$class,
),
array(
$icon,
$label,
));
}
}
$content = $this->markupEngine->getOutput(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
if ($this->preview) {
$anchor = null;
} else {
$anchor = phutil_tag(
'a',
array(
'name' => $anchor_name,
'id' => $anchor_name,
'class' => 'differential-inline-comment-anchor',
),
'');
}
if ($inline->isDraft() && !$is_synthetic) {
$classes[] = 'inline-state-is-draft';
}
if ($is_synthetic) {
$classes[] = 'differential-inline-comment-synthetic';
}
$classes = implode(' ', $classes);
$author_owner = null;
if ($is_synthetic) {
$author = $inline->getSyntheticAuthor();
} else {
$author = $handles[$inline->getAuthorPHID()]->getName();
if ($inline->getAuthorPHID() == $this->objectOwnerPHID) {
$author_owner = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setName(pht('Author'))
->setSlimShady(true)
->setColor(PHUITagView::COLOR_YELLOW)
->addClass('mml');
}
}
$actions = null;
if ($action_buttons) {
$actions = new PHUIButtonBarView();
$actions->setBorderless(true);
$actions->addClass('inline-button-divider');
foreach ($action_buttons as $button) {
$actions->addButton($button);
}
}
$group_left = phutil_tag(
'div',
array(
'class' => 'inline-head-left',
),
array(
$author,
$author_owner,
$draft_text,
$ghost_tag,
));
$group_right = phutil_tag(
'div',
array(
'class' => 'inline-head-right',
),
array(
$done_button,
$links,
$actions,
));
$snippet = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(96)
->truncateString($inline->getContent());
$metadata['snippet'] = pht('%s: %s', $author, $snippet);
$markup = javelin_tag(
'div',
array(
'class' => $classes,
'sigil' => $sigil,
'meta' => $metadata,
),
array(
javelin_tag(
'div',
array(
'class' => 'differential-inline-comment-head grouped',
'sigil' => 'differential-inline-header',
),
array(
$group_left,
$group_right,
)),
phutil_tag_div(
'differential-inline-comment-content',
phutil_tag_div('phabricator-remarkup', $content)),
));
$summary = phutil_tag(
'div',
array(
'class' => 'differential-inline-summary',
),
array(
phutil_tag('strong', array(), pht('%s:', $author)),
' ',
$snippet,
));
return array(
$anchor,
$markup,
$summary,
);
}
private function canHide() {
$inline = $this->inlineComment;
if ($inline->isDraft()) {
return false;
}
if (!$inline->getID()) {
return false;
}
$viewer = $this->getUser();
if (!$viewer->isLoggedIn()) {
return false;
}
if (!$inline->supportsHiding()) {
return false;
}
return true;
}
}
diff --git a/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php b/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php
index 8f6e4e48d..6d76319de 100644
--- a/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php
+++ b/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php
@@ -1,333 +1,334 @@
<?php
final class PHUIDiffTableOfContentsItemView extends AphrontView {
private $changeset;
private $isVisible = true;
private $anchor;
private $coverage;
private $coverageID;
private $context;
private $packages;
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
return $this;
}
public function getChangeset() {
return $this->changeset;
}
public function setIsVisible($is_visible) {
$this->isVisible = $is_visible;
return $this;
}
public function getIsVisible() {
return $this->isVisible;
}
public function setAnchor($anchor) {
$this->anchor = $anchor;
return $this;
}
public function getAnchor() {
return $this->anchor;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
public function getCoverage() {
return $this->coverage;
}
public function setCoverageID($coverage_id) {
$this->coverageID = $coverage_id;
return $this;
}
public function getCoverageID() {
return $this->coverageID;
}
public function setContext($context) {
$this->context = $context;
return $this;
}
public function getContext() {
return $this->context;
}
public function setPackages(array $packages) {
assert_instances_of($packages, 'PhabricatorOwnersPackage');
$this->packages = mpull($packages, null, 'getPHID');
return $this;
}
public function getPackages() {
return $this->packages;
}
public function render() {
$changeset = $this->getChangeset();
$cells = array();
$cells[] = $this->getContext();
$cells[] = $this->renderPathChangeCharacter();
$cells[] = $this->renderPropertyChangeCharacter();
$cells[] = $this->renderPropertyChangeDescription();
$link = $this->renderChangesetLink();
$lines = $this->renderChangesetLines();
$meta = $this->renderChangesetMetadata();
$cells[] = array(
$link,
$lines,
$meta,
);
$cells[] = $this->renderCoverage();
$cells[] = $this->renderModifiedCoverage();
$cells[] = $this->renderPackages();
return $cells;
}
private function renderPathChangeCharacter() {
$changeset = $this->getChangeset();
$type = $changeset->getChangeType();
$color = DifferentialChangeType::getSummaryColorForChangeType($type);
$char = DifferentialChangeType::getSummaryCharacterForChangeType($type);
$title = DifferentialChangeType::getFullNameForChangeType($type);
return javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $title,
'align' => 'E',
),
'class' => 'phui-text-'.$color,
),
$char);
}
private function renderPropertyChangeCharacter() {
$changeset = $this->getChangeset();
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
if ($old === $new) {
return null;
}
return javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Properties Modified'),
'align' => 'E',
'size' => 200,
),
),
'M');
}
private function renderPropertyChangeDescription() {
$changeset = $this->getChangeset();
$file_type = $changeset->getFileType();
$desc = DifferentialChangeType::getShortNameForFileType($file_type);
if ($desc === null) {
return null;
}
return pht('(%s)', $desc);
}
private function renderChangesetLink() {
$anchor = $this->getAnchor();
$changeset = $this->getChangeset();
$name = $changeset->getDisplayFilename();
$change_type = $changeset->getChangeType();
if (DifferentialChangeType::isOldLocationChangeType($change_type)) {
$away = $changeset->getAwayPaths();
if (count($away) == 1) {
if ($change_type == DifferentialChangeType::TYPE_MOVE_AWAY) {
$right_arrow = "\xE2\x86\x92";
$name = $this->renderRename($name, head($away), $right_arrow);
}
}
} else if ($change_type == DifferentialChangeType::TYPE_MOVE_HERE) {
$left_arrow = "\xE2\x86\x90";
$name = $this->renderRename($name, $changeset->getOldFile(), $left_arrow);
}
return javelin_tag(
'a',
array(
'href' => '#'.$anchor,
'sigil' => 'differential-load',
'meta' => array(
'id' => 'diff-'.$anchor,
),
),
$name);
}
private function renderChangesetLines() {
$changeset = $this->getChangeset();
$line_count = $changeset->getAffectedLineCount();
if (!$line_count) {
return null;
}
return ' '.pht('(%d line(s))', $line_count);
}
private function renderCoverage() {
$not_applicable = '-';
$coverage = $this->getCoverage();
if (!strlen($coverage)) {
return $not_applicable;
}
$covered = substr_count($coverage, 'C');
$not_covered = substr_count($coverage, 'U');
if (!$not_covered && !$covered) {
return $not_applicable;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
private function renderModifiedCoverage() {
$not_applicable = '-';
$coverage = $this->getCoverage();
if (!strlen($coverage)) {
return $not_applicable;
}
if ($this->getIsVisible()) {
$label = pht('Loading...');
} else {
$label = pht('?');
}
return phutil_tag(
'div',
array(
'id' => $this->getCoverageID(),
'class' => 'differential-mcoverage-loading',
),
$label);
}
private function renderChangesetMetadata() {
$changeset = $this->getChangeset();
$type = $changeset->getChangeType();
$meta = array();
if (DifferentialChangeType::isOldLocationChangeType($type)) {
$away = $changeset->getAwayPaths();
if (count($away) > 1) {
if ($type == DifferentialChangeType::TYPE_MULTICOPY) {
$meta[] = pht('Deleted after being copied to multiple locations:');
} else {
$meta[] = pht('Copied to multiple locations:');
}
foreach ($away as $path) {
$meta[] = $path;
}
} else {
if ($type == DifferentialChangeType::TYPE_MOVE_AWAY) {
// This case is handled when we render the path.
} else {
$meta[] = pht('Copied to %s', head($away));
}
}
} else if ($type == DifferentialChangeType::TYPE_COPY_HERE) {
$meta[] = pht('Copied from %s', $changeset->getOldFile());
}
if (!$meta) {
return null;
}
$meta = phutil_implode_html(phutil_tag('br'), $meta);
return phutil_tag(
'div',
array(
'class' => 'differential-toc-meta',
),
$meta);
}
private function renderPackages() {
$packages = $this->getPackages();
if (!$packages) {
return null;
}
$viewer = $this->getUser();
$package_phids = mpull($packages, 'getPHID');
- return $viewer->renderHandleList($package_phids);
+ return $viewer->renderHandleList($package_phids)
+ ->setGlyphLimit(48);
}
private function renderRename($self, $other, $arrow) {
$old = explode('/', $self);
$new = explode('/', $other);
$start = count($old);
foreach ($old as $index => $part) {
if (!isset($new[$index]) || $part != $new[$index]) {
$start = $index;
break;
}
}
$end = count($old);
foreach (array_reverse($old) as $from_end => $part) {
$index = count($new) - $from_end - 1;
if (!isset($new[$index]) || $part != $new[$index]) {
$end = $from_end;
break;
}
}
$rename =
'{'.
implode('/', array_slice($old, $start, count($old) - $end - $start)).
' '.$arrow.' '.
implode('/', array_slice($new, $start, count($new) - $end - $start)).
'}';
array_splice($new, $start, count($new) - $end - $start, $rename);
return implode('/', $new);
}
}
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index cd75800e4..1236c620f 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,971 +1,979 @@
<?php
/**
* Manages the execution environment configuration, exposing APIs to read
* configuration settings and other similar values that are derived directly
* from configuration settings.
*
*
* = Reading Configuration =
*
* The primary role of this class is to provide an API for reading
* Phabricator configuration, @{method:getEnvConfig}:
*
* $value = PhabricatorEnv::getEnvConfig('some.key', $default);
*
* The class also handles some URI construction based on configuration, via
* the methods @{method:getURI}, @{method:getProductionURI},
* @{method:getCDNURI}, and @{method:getDoclink}.
*
* For configuration which allows you to choose a class to be responsible for
* some functionality (e.g., which mail adapter to use to deliver email),
* @{method:newObjectFromConfig} provides a simple interface that validates
* the configured value.
*
*
* = Unit Test Support =
*
* In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
* mutable environment. The method returns a scope guard object which restores
* the environment when it is destroyed. For example:
*
* public function testExample() {
* $env = PhabricatorEnv::beginScopedEnv();
* $env->overrideEnv('some.key', 'new-value-for-this-test');
*
* // Some test which depends on the value of 'some.key'.
*
* }
*
* Your changes will persist until the `$env` object leaves scope or is
* destroyed.
*
* You should //not// use this in normal code.
*
*
* @task read Reading Configuration
* @task uri URI Validation
* @task test Unit Test Support
* @task internal Internals
*/
final class PhabricatorEnv extends Phobject {
private static $sourceStack;
private static $repairSource;
private static $overrideSource;
private static $requestBaseURI;
private static $cache;
private static $localeCode;
private static $readOnly;
private static $readOnlyReason;
const READONLY_CONFIG = 'config';
const READONLY_UNREACHABLE = 'unreachable';
const READONLY_SEVERED = 'severed';
const READONLY_MASTERLESS = 'masterless';
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function initializeWebEnvironment() {
self::initializeCommonEnvironment(false);
}
public static function initializeScriptEnvironment($config_optional) {
self::initializeCommonEnvironment($config_optional);
// NOTE: This is dangerous in general, but we know we're in a script context
// and are not vulnerable to CSRF.
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
// There are several places where we log information (about errors, events,
// service calls, etc.) for analysis via DarkConsole or similar. These are
// useful for web requests, but grow unboundedly in long-running scripts and
// daemons. Discard data as it arrives in these cases.
PhutilServiceProfiler::getInstance()->enableDiscardMode();
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
DarkConsoleEventPluginAPI::enableDiscardMode();
}
private static function initializeCommonEnvironment($config_optional) {
PhutilErrorHandler::initialize();
self::resetUmask();
self::buildConfigurationSourceStack($config_optional);
// Force a valid timezone. If both PHP and Phabricator configuration are
// invalid, use UTC.
$tz = self::getEnvConfig('phabricator.timezone');
if ($tz) {
@date_default_timezone_set($tz);
}
$ok = @date_default_timezone_set(date_default_timezone_get());
if (!$ok) {
date_default_timezone_set('UTC');
}
// Prepend '/support/bin' and append any paths to $PATH if we need to.
$env_path = getenv('PATH');
$phabricator_path = dirname(phutil_get_library_root('phabricator'));
$support_path = $phabricator_path.'/support/bin';
$env_path = $support_path.PATH_SEPARATOR.$env_path;
$append_dirs = self::getEnvConfig('environment.append-paths');
if (!empty($append_dirs)) {
$append_path = implode(PATH_SEPARATOR, $append_dirs);
$env_path = $env_path.PATH_SEPARATOR.$append_path;
}
putenv('PATH='.$env_path);
// Write this back into $_ENV, too, so ExecFuture picks it up when creating
// subprocess environments.
$_ENV['PATH'] = $env_path;
// If an instance identifier is defined, write it into the environment so
// it's available to subprocesses.
$instance = self::getEnvConfig('cluster.instance');
if (strlen($instance)) {
putenv('PHABRICATOR_INSTANCE='.$instance);
$_ENV['PHABRICATOR_INSTANCE'] = $instance;
}
PhabricatorEventEngine::initialize();
// TODO: Add a "locale.default" config option once we have some reasonable
// defaults which aren't silly nonsense.
self::setLocaleCode('en_US');
}
public static function beginScopedLocale($locale_code) {
return new PhabricatorLocaleScopeGuard($locale_code);
}
public static function getLocaleCode() {
return self::$localeCode;
}
public static function setLocaleCode($locale_code) {
if (!$locale_code) {
return;
}
if ($locale_code == self::$localeCode) {
return;
}
try {
$locale = PhutilLocale::loadLocale($locale_code);
$translations = PhutilTranslation::getTranslationMapForLocale(
$locale_code);
$override = self::getEnvConfig('translation.override');
if (!is_array($override)) {
$override = array();
}
PhutilTranslator::getInstance()
->setLocale($locale)
->setTranslations($override + $translations);
self::$localeCode = $locale_code;
} catch (Exception $ex) {
// Just ignore this; the user likely has an out-of-date locale code.
}
}
private static function buildConfigurationSourceStack($config_optional) {
self::dropConfigCache();
$stack = new PhabricatorConfigStackSource();
self::$sourceStack = $stack;
$default_source = id(new PhabricatorConfigDefaultSource())
->setName(pht('Global Default'));
$stack->pushSource($default_source);
$env = self::getSelectedEnvironmentName();
if ($env) {
$stack->pushSource(
id(new PhabricatorConfigFileSource($env))
->setName(pht("File '%s'", $env)));
}
$stack->pushSource(
id(new PhabricatorConfigLocalSource())
->setName(pht('Local Config')));
// If the install overrides the database adapter, we might need to load
// the database adapter class before we can push on the database config.
// This config is locked and can't be edited from the web UI anyway.
foreach (self::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
// Drop any class map caches, since they will have generated without
// any classes from libraries. Without this, preflight setup checks can
// cause generation of a setup check cache that omits checks defined in
// libraries, for example.
PhutilClassMapQuery::deleteCaches();
// If custom libraries specify config options, they won't get default
// values as the Default source has already been loaded, so we get it to
// pull in all options from non-phabricator libraries now they are loaded.
$default_source->loadExternalOptions();
// If this install has site config sources, load them now.
$site_sources = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorConfigSiteSource')
->setSortMethod('getPriority')
->execute();
foreach ($site_sources as $site_source) {
$stack->pushSource($site_source);
+
+ // If the site source did anything which reads config, throw it away
+ // to make sure any additional site sources get clean reads.
+ self::dropConfigCache();
}
$masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();
if (!$masters) {
self::setReadOnly(true, self::READONLY_MASTERLESS);
} else {
// If any master is severed, we drop to readonly mode. In theory we
// could try to continue if we're only missing some applications, but
// this is very complex and we're unlikely to get it right.
foreach ($masters as $master) {
// Give severed masters one last chance to get healthy.
if ($master->isSevered()) {
$master->checkHealth();
}
if ($master->isSevered()) {
self::setReadOnly(true, self::READONLY_SEVERED);
break;
}
}
}
try {
$stack->pushSource(
id(new PhabricatorConfigDatabaseSource('default'))
->setName(pht('Database')));
} catch (AphrontSchemaQueryException $exception) {
// If the database is not available, just skip this configuration
// source. This happens during `bin/storage upgrade`, `bin/conf` before
// schema setup, etc.
} catch (PhabricatorClusterStrandedException $ex) {
// This means we can't connect to any database host. That's fine as
// long as we're running a setup script like `bin/storage`.
if (!$config_optional) {
throw $ex;
}
}
+
+ // Drop the config cache one final time to make sure we're getting clean
+ // reads now that we've finished building the stack.
+ self::dropConfigCache();
}
public static function repairConfig($key, $value) {
if (!self::$repairSource) {
self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
->setName(pht('Repaired Config'));
self::$sourceStack->pushSource(self::$repairSource);
}
self::$repairSource->setKeys(array($key => $value));
self::dropConfigCache();
}
public static function overrideConfig($key, $value) {
if (!self::$overrideSource) {
self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
->setName(pht('Overridden Config'));
self::$sourceStack->pushSource(self::$overrideSource);
}
self::$overrideSource->setKeys(array($key => $value));
self::dropConfigCache();
}
public static function getUnrepairedEnvConfig($key, $default = null) {
foreach (self::$sourceStack->getStack() as $source) {
if ($source === self::$repairSource) {
continue;
}
$result = $source->getKeys(array($key));
if ($result) {
return $result[$key];
}
}
return $default;
}
public static function getSelectedEnvironmentName() {
$env_var = 'PHABRICATOR_ENV';
$env = idx($_SERVER, $env_var);
if (!$env) {
$env = getenv($env_var);
}
if (!$env) {
$env = idx($_ENV, $env_var);
}
if (!$env) {
$root = dirname(phutil_get_library_root('phabricator'));
$path = $root.'/conf/local/ENVIRONMENT';
if (Filesystem::pathExists($path)) {
$env = trim(Filesystem::readFile($path));
}
}
return $env;
}
/* -( Reading Configuration )---------------------------------------------- */
/**
* Get the current configuration setting for a given key.
*
* If the key is not found, then throw an Exception.
*
* @task read
*/
public static function getEnvConfig($key) {
if (!self::$sourceStack) {
throw new Exception(
pht(
'Trying to read configuration "%s" before configuration has been '.
'initialized.',
$key));
}
if (isset(self::$cache[$key])) {
return self::$cache[$key];
}
if (array_key_exists($key, self::$cache)) {
return self::$cache[$key];
}
if (!self::$sourceStack) {
throw new Exception(
pht(
'Trying to read configuration "%s" before configuration has been '.
'initialized.',
$key));
}
$result = self::$sourceStack->getKeys(array($key));
if (array_key_exists($key, $result)) {
self::$cache[$key] = $result[$key];
return $result[$key];
} else {
throw new Exception(
pht(
"No config value specified for key '%s'.",
$key));
}
}
/**
* Get the current configuration setting for a given key. If the key
* does not exist, return a default value instead of throwing. This is
* primarily useful for migrations involving keys which are slated for
* removal.
*
* @task read
*/
public static function getEnvConfigIfExists($key, $default = null) {
try {
return self::getEnvConfig($key);
} catch (Exception $ex) {
return $default;
}
}
/**
* Get the fully-qualified URI for a path.
*
* @task read
*/
public static function getURI($path) {
return rtrim(self::getAnyBaseURI(), '/').$path;
}
/**
* Get the fully-qualified production URI for a path.
*
* @task read
*/
public static function getProductionURI($path) {
// If we're passed a URI which already has a domain, simply return it
// unmodified. In particular, files may have URIs which point to a CDN
// domain.
$uri = new PhutilURI($path);
if ($uri->getDomain()) {
return $path;
}
$production_domain = self::getEnvConfig('phabricator.production-uri');
if (!$production_domain) {
$production_domain = self::getAnyBaseURI();
}
return rtrim($production_domain, '/').$path;
}
public static function isSelfURI($raw_uri) {
$uri = new PhutilURI($raw_uri);
$host = $uri->getDomain();
if (!strlen($host)) {
return false;
}
$host = phutil_utf8_strtolower($host);
$self_map = self::getSelfURIMap();
return isset($self_map[$host]);
}
private static function getSelfURIMap() {
$self_uris = array();
$self_uris[] = self::getProductionURI('/');
$self_uris[] = self::getURI('/');
$allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
foreach ($allowed_uris as $allowed_uri) {
$self_uris[] = $allowed_uri;
}
$self_map = array();
foreach ($self_uris as $self_uri) {
$host = id(new PhutilURI($self_uri))->getDomain();
if (!strlen($host)) {
continue;
}
$host = phutil_utf8_strtolower($host);
$self_map[$host] = $host;
}
return $self_map;
}
/**
* Get the fully-qualified production URI for a static resource path.
*
* @task read
*/
public static function getCDNURI($path) {
$alt = self::getEnvConfig('security.alternate-file-domain');
if (!$alt) {
$alt = self::getAnyBaseURI();
}
$uri = new PhutilURI($alt);
$uri->setPath($path);
return (string)$uri;
}
/**
* Get the fully-qualified production URI for a documentation resource.
*
* @task read
*/
public static function getDoclink($resource, $type = 'article') {
$uri = new PhutilURI('https://secure.phabricator.com/diviner/find/');
$uri->setQueryParam('name', $resource);
$uri->setQueryParam('type', $type);
$uri->setQueryParam('jump', true);
return (string)$uri;
}
/**
* Build a concrete object from a configuration key.
*
* @task read
*/
public static function newObjectFromConfig($key, $args = array()) {
$class = self::getEnvConfig($key);
return newv($class, $args);
}
public static function getAnyBaseURI() {
$base_uri = self::getEnvConfig('phabricator.base-uri');
if (!$base_uri) {
$base_uri = self::getRequestBaseURI();
}
if (!$base_uri) {
throw new Exception(
pht(
"Define '%s' in your configuration to continue.",
'phabricator.base-uri'));
}
return $base_uri;
}
public static function getRequestBaseURI() {
return self::$requestBaseURI;
}
public static function setRequestBaseURI($uri) {
self::$requestBaseURI = $uri;
}
public static function isReadOnly() {
if (self::$readOnly !== null) {
return self::$readOnly;
}
return self::getEnvConfig('cluster.read-only');
}
public static function setReadOnly($read_only, $reason) {
self::$readOnly = $read_only;
self::$readOnlyReason = $reason;
}
public static function getReadOnlyMessage() {
$reason = self::getReadOnlyReason();
switch ($reason) {
case self::READONLY_MASTERLESS:
return pht(
'Phabricator is in read-only mode (no writable database '.
'is configured).');
case self::READONLY_UNREACHABLE:
return pht(
'Phabricator is in read-only mode (unreachable master).');
case self::READONLY_SEVERED:
return pht(
'Phabricator is in read-only mode (major interruption).');
}
return pht('Phabricator is in read-only mode.');
}
public static function getReadOnlyURI() {
return urisprintf(
'/readonly/%s/',
self::getReadOnlyReason());
}
public static function getReadOnlyReason() {
if (!self::isReadOnly()) {
return null;
}
if (self::$readOnlyReason !== null) {
return self::$readOnlyReason;
}
return self::READONLY_CONFIG;
}
/* -( Unit Test Support )-------------------------------------------------- */
/**
* @task test
*/
public static function beginScopedEnv() {
return new PhabricatorScopedEnv(self::pushTestEnvironment());
}
/**
* @task test
*/
private static function pushTestEnvironment() {
self::dropConfigCache();
$source = new PhabricatorConfigDictionarySource(array());
self::$sourceStack->pushSource($source);
return spl_object_hash($source);
}
/**
* @task test
*/
public static function popTestEnvironment($key) {
self::dropConfigCache();
$source = self::$sourceStack->popSource();
$stack_key = spl_object_hash($source);
if ($stack_key !== $key) {
self::$sourceStack->pushSource($source);
throw new Exception(
pht(
'Scoped environments were destroyed in a different order than they '.
'were initialized.'));
}
}
/* -( URI Validation )----------------------------------------------------- */
/**
* Detect if a URI satisfies either @{method:isValidLocalURIForLink} or
* @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the
* URI of some other resource which has a valid protocol. This rejects
* garbage URIs and URIs with protocols which do not appear in the
* `uri.allowed-protocols` configuration, notably 'javascript:' URIs.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to put in an "href" link attribute.
*
* @param string URI to test.
* @return bool True if the URI identifies a web resource.
* @task uri
*/
public static function isValidURIForLink($uri) {
return self::isValidLocalURIForLink($uri) ||
self::isValidRemoteURIForLink($uri);
}
/**
* Detect if a URI identifies some page on this server.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to issue a "Location:" redirect to.
*
* @param string URI to test.
* @return bool True if the URI identifies a local page.
* @task uri
*/
public static function isValidLocalURIForLink($uri) {
$uri = (string)$uri;
if (!strlen($uri)) {
return false;
}
if (preg_match('/\s/', $uri)) {
// PHP hasn't been vulnerable to header injection attacks for a bunch of
// years, but we can safely reject these anyway since they're never valid.
return false;
}
// Chrome (at a minimum) interprets backslashes in Location headers and the
// URL bar as forward slashes. This is probably intended to reduce user
// error caused by confusion over which key is "forward slash" vs "back
// slash".
//
// However, it means a URI like "/\evil.com" is interpreted like
// "//evil.com", which is a protocol relative remote URI.
//
// Since we currently never generate URIs with backslashes in them, reject
// these unconditionally rather than trying to figure out how browsers will
// interpret them.
if (preg_match('/\\\\/', $uri)) {
return false;
}
// Valid URIs must begin with '/', followed by the end of the string or some
// other non-'/' character. This rejects protocol-relative URIs like
// "//evil.com/evil_stuff/".
return (bool)preg_match('@^/([^/]|$)@', $uri);
}
/**
* Detect if a URI identifies some valid linkable remote resource.
*
* @param string URI to test.
* @return bool True if a URI identifies a remote resource with an allowed
* protocol.
* @task uri
*/
public static function isValidRemoteURIForLink($uri) {
try {
self::requireValidRemoteURIForLink($uri);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Detect if a URI identifies a valid linkable remote resource, throwing a
* detailed message if it does not.
*
* A valid linkable remote resource can be safely linked or redirected to.
* This is primarily a protocol whitelist check.
*
* @param string URI to test.
* @return void
* @task uri
*/
public static function requireValidRemoteURIForLink($raw_uri) {
$uri = new PhutilURI($raw_uri);
$proto = $uri->getProtocol();
if (!strlen($proto)) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must specify a protocol.',
$raw_uri));
}
$protocols = self::getEnvConfig('uri.allowed-protocols');
if (!isset($protocols[$proto])) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must use one of these protocols: %s.',
$raw_uri,
implode(', ', array_keys($protocols))));
}
$domain = $uri->getDomain();
if (!strlen($domain)) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must specify a domain.',
$raw_uri));
}
}
/**
* Detect if a URI identifies a valid fetchable remote resource.
*
* @param string URI to test.
* @param list<string> Allowed protocols.
* @return bool True if the URI is a valid fetchable remote resource.
* @task uri
*/
public static function isValidRemoteURIForFetch($uri, array $protocols) {
try {
self::requireValidRemoteURIForFetch($uri, $protocols);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Detect if a URI identifies a valid fetchable remote resource, throwing
* a detailed message if it does not.
*
* A valid fetchable remote resource can be safely fetched using a request
* originating on this server. This is a primarily an address check against
* the outbound address blacklist.
*
* @param string URI to test.
* @param list<string> Allowed protocols.
* @return pair<string, string> Pre-resolved URI and domain.
* @task uri
*/
public static function requireValidRemoteURIForFetch(
$raw_uri,
array $protocols) {
$uri = new PhutilURI($raw_uri);
$proto = $uri->getProtocol();
if (!strlen($proto)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must specify a protocol.',
$raw_uri));
}
$protocols = array_fuse($protocols);
if (!isset($protocols[$proto])) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must use one of these protocols: %s.',
$raw_uri,
implode(', ', array_keys($protocols))));
}
$domain = $uri->getDomain();
if (!strlen($domain)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must specify a domain.',
$raw_uri));
}
$addresses = gethostbynamel($domain);
if (!$addresses) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. The domain "%s" could '.
'not be resolved.',
$raw_uri,
$domain));
}
foreach ($addresses as $address) {
if (self::isBlacklistedOutboundAddress($address)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. The domain "%s" '.
'resolves to the address "%s", which is blacklisted for '.
'outbound requests.',
$raw_uri,
$domain,
$address));
}
}
$resolved_uri = clone $uri;
$resolved_uri->setDomain(head($addresses));
return array($resolved_uri, $domain);
}
/**
* Determine if an IP address is in the outbound address blacklist.
*
* @param string IP address.
* @return bool True if the address is blacklisted.
*/
public static function isBlacklistedOutboundAddress($address) {
$blacklist = self::getEnvConfig('security.outbound-blacklist');
return PhutilCIDRList::newList($blacklist)->containsAddress($address);
}
public static function isClusterRemoteAddress() {
$cluster_addresses = self::getEnvConfig('cluster.addresses');
if (!$cluster_addresses) {
return false;
}
$address = self::getRemoteAddress();
if (!$address) {
throw new Exception(
pht(
'Unable to test remote address against cluster whitelist: '.
'REMOTE_ADDR is not defined or not valid.'));
}
return self::isClusterAddress($address);
}
public static function isClusterAddress($address) {
$cluster_addresses = self::getEnvConfig('cluster.addresses');
if (!$cluster_addresses) {
throw new Exception(
pht(
'Phabricator is not configured to serve cluster requests. '.
'Set `cluster.addresses` in the configuration to whitelist '.
'cluster hosts before sending requests that use a cluster '.
'authentication mechanism.'));
}
return PhutilCIDRList::newList($cluster_addresses)
->containsAddress($address);
}
public static function getRemoteAddress() {
$address = idx($_SERVER, 'REMOTE_ADDR');
if (!$address) {
return null;
}
try {
return PhutilIPAddress::newAddress($address);
} catch (Exception $ex) {
return null;
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
public static function envConfigExists($key) {
return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
}
/**
* @task internal
*/
public static function getAllConfigKeys() {
return self::$sourceStack->getAllKeys();
}
public static function getConfigSourceStack() {
return self::$sourceStack;
}
/**
* @task internal
*/
public static function overrideTestEnvConfig($stack_key, $key, $value) {
$tmp = array();
// If we don't have the right key, we'll throw when popping the last
// source off the stack.
do {
$source = self::$sourceStack->popSource();
array_unshift($tmp, $source);
if (spl_object_hash($source) == $stack_key) {
$source->setKeys(array($key => $value));
break;
}
} while (true);
foreach ($tmp as $source) {
self::$sourceStack->pushSource($source);
}
self::dropConfigCache();
}
private static function dropConfigCache() {
self::$cache = array();
}
private static function resetUmask() {
// Reset the umask to the common standard umask. The umask controls default
// permissions when files are created and propagates to subprocesses.
// "022" is the most common umask, but sometimes it is set to something
// unusual by the calling environment.
// Since various things rely on this umask to work properly and we are
// not aware of any legitimate reasons to adjust it, unconditionally
// normalize it until such reasons arise. See T7475 for discussion.
umask(022);
}
/**
* Get the path to an empty directory which is readable by all of the system
* user accounts that Phabricator acts as.
*
* In some cases, a binary needs some valid HOME or CWD to continue, but not
* all user accounts have valid home directories and even if they do they
* may not be readable after a `sudo` operation.
*
* @return string Path to an empty directory suitable for use as a CWD.
*/
public static function getEmptyCWD() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/support/empty/';
}
}
diff --git a/src/infrastructure/export/field/PhabricatorDoubleExportField.php b/src/infrastructure/export/field/PhabricatorDoubleExportField.php
new file mode 100644
index 000000000..18087dd70
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorDoubleExportField.php
@@ -0,0 +1,25 @@
+<?php
+
+final class PhabricatorDoubleExportField
+ extends PhabricatorExportField {
+
+ public function getNaturalValue($value) {
+ if ($value === null) {
+ return $value;
+ }
+
+ return (double)$value;
+ }
+
+ /**
+ * @phutil-external-symbol class PHPExcel_Cell_DataType
+ */
+ public function formatPHPExcelCell($cell, $style) {
+ $cell->setDataType(PHPExcel_Cell_DataType::TYPE_NUMERIC);
+ }
+
+ public function getCharacterWidth() {
+ return 8;
+ }
+
+}
diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php
index 2b0c78788..606df393d 100644
--- a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php
+++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php
@@ -1,168 +1,172 @@
<?php
final class PhabricatorExcelExportFormat
extends PhabricatorExportFormat {
const EXPORTKEY = 'excel';
private $workbook;
private $sheet;
private $rowCursor;
public function getExportFormatName() {
return pht('Excel (.xlsx)');
}
public function isExportFormatEnabled() {
// TODO: PHPExcel has a dependency on the PHP zip extension. We should test
// for that here, since it fatals if we don't have the ZipArchive class.
return @include_once 'PHPExcel.php';
}
public function getInstallInstructions() {
return pht(<<<EOHELP
Data can not be exported to Excel because the PHPExcel library is not
installed. This software component is required for Phabricator to create
Excel files.
You can install PHPExcel from GitHub:
> https://github.com/PHPOffice/PHPExcel
Briefly:
- Clone that repository somewhere on the sever
(like `/path/to/example/PHPExcel`).
- Update your PHP `%s` setting (in `php.ini`) to include the PHPExcel
`Classes` directory (like `/path/to/example/PHPExcel/Classes`).
EOHELP
,
'include_path');
}
public function getFileExtension() {
return 'xlsx';
}
public function getMIMEContentType() {
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
/**
* @phutil-external-symbol class PHPExcel_Cell_DataType
*/
public function addHeaders(array $fields) {
$sheet = $this->getSheet();
$header_format = array(
'font' => array(
'bold' => true,
),
);
$row = 1;
$col = 0;
foreach ($fields as $field) {
$cell_value = $field->getLabel();
$cell_name = $this->getCellName($col, $row);
$cell = $sheet->setCellValue(
$cell_name,
$cell_value,
$return_cell = true);
$sheet->getStyle($cell_name)->applyFromArray($header_format);
$cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING);
$width = $field->getCharacterWidth();
if ($width !== null) {
$col_name = $this->getCellName($col);
$sheet->getColumnDimension($col_name)
->setWidth($width);
}
$col++;
}
}
public function addObject($object, array $fields, array $map) {
$sheet = $this->getSheet();
$col = 0;
foreach ($fields as $key => $field) {
$cell_value = $map[$key];
$cell_value = $field->getPHPExcelValue($cell_value);
$cell_name = $this->getCellName($col, $this->rowCursor);
$cell = $sheet->setCellValue(
$cell_name,
$cell_value,
$return_cell = true);
$style = $sheet->getStyle($cell_name);
$field->formatPHPExcelCell($cell, $style);
$col++;
}
$this->rowCursor++;
}
/**
* @phutil-external-symbol class PHPExcel_IOFactory
*/
public function newFileData() {
$workbook = $this->getWorkbook();
$writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007');
ob_start();
$writer->save('php://output');
$data = ob_get_clean();
return $data;
}
private function getWorkbook() {
if (!$this->workbook) {
$this->workbook = $this->newWorkbook();
}
return $this->workbook;
}
/**
* @phutil-external-symbol class PHPExcel
*/
private function newWorkbook() {
include_once 'PHPExcel.php';
return new PHPExcel();
}
private function getSheet() {
if (!$this->sheet) {
$workbook = $this->getWorkbook();
$sheet = $workbook->setActiveSheetIndex(0);
$sheet->setTitle($this->getTitle());
$this->sheet = $sheet;
// The row cursor starts on the second row, after the header row.
$this->rowCursor = 2;
}
return $this->sheet;
}
+
+ /**
+ * @phutil-external-symbol class PHPExcel_Cell
+ */
private function getCellName($col, $row = null) {
- $col_name = chr(ord('A') + $col);
+ $col_name = PHPExcel_Cell::stringFromColumnIndex($col);
if ($row === null) {
return $col_name;
}
return $col_name.$row;
}
}
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
index eb74f8deb..c2241995e 100644
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -1,1657 +1,1683 @@
<?php
final class PhabricatorUSEnglishTranslation
extends PhutilTranslation {
public function getLocaleCode() {
return 'en_US';
}
protected function getTranslations() {
return array(
'No daemon(s) with id(s) "%s" exist!' => array(
'No daemon with id %s exists!',
'No daemons with ids %s exist!',
),
'These %d configuration value(s) are related:' => array(
'This configuration value is related:',
'These configuration values are related:',
),
'%s Task(s)' => array('Task', 'Tasks'),
'%s ERROR(S)' => array('ERROR', 'ERRORS'),
'%d Error(s)' => array('%d Error', '%d Errors'),
'%d Warning(s)' => array('%d Warning', '%d Warnings'),
'%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'),
'%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'),
'%d Detail(s)' => array('%d Detail', '%d Details'),
'(%d line(s))' => array('(%d line)', '(%d lines)'),
'%d line(s)' => array('%d line', '%d lines'),
'%d path(s)' => array('%d path', '%d paths'),
'%d diff(s)' => array('%d diff', '%d diffs'),
'%s Answer(s)' => array('%s Answer', '%s Answers'),
'Show %d Comment(s)' => array('Show %d Comment', 'Show %d Comments'),
'%s DIFF LINK(S)' => array('DIFF LINK', 'DIFF LINKS'),
'You successfully created %d diff(s).' => array(
'You successfully created %d diff.',
'You successfully created %d diffs.',
),
'Diff creation failed; see body for %s error(s).' => array(
'Diff creation failed; see body for error.',
'Diff creation failed; see body for errors.',
),
'There are %d raw fact(s) in storage.' => array(
'There is %d raw fact in storage.',
'There are %d raw facts in storage.',
),
'There are %d aggregate fact(s) in storage.' => array(
'There is %d aggregate fact in storage.',
'There are %d aggregate facts in storage.',
),
'%s Commit(s) Awaiting Audit' => array(
'%s Commit Awaiting Audit',
'%s Commits Awaiting Audit',
),
'%s Problem Commit(s)' => array(
'%s Problem Commit',
'%s Problem Commits',
),
'%s Review(s) Blocking Others' => array(
'%s Review Blocking Others',
'%s Reviews Blocking Others',
),
'%s Review(s) Need Attention' => array(
'%s Review Needs Attention',
'%s Reviews Need Attention',
),
'%s Review(s) Waiting on Others' => array(
'%s Review Waiting on Others',
'%s Reviews Waiting on Others',
),
'%s Active Review(s)' => array(
'%s Active Review',
'%s Active Reviews',
),
'%s Flagged Object(s)' => array(
'%s Flagged Object',
'%s Flagged Objects',
),
'%s Object(s) Tracked' => array(
'%s Object Tracked',
'%s Objects Tracked',
),
'%s Assigned Task(s)' => array(
'%s Assigned Task',
'%s Assigned Tasks',
),
'Show %d Lint Message(s)' => array(
'Show %d Lint Message',
'Show %d Lint Messages',
),
'Hide %d Lint Message(s)' => array(
'Hide %d Lint Message',
'Hide %d Lint Messages',
),
'This is a binary file. It is %s byte(s) in length.' => array(
'This is a binary file. It is %s byte in length.',
'This is a binary file. It is %s bytes in length.',
),
'%s Action(s) Have No Effect' => array(
'Action Has No Effect',
'Actions Have No Effect',
),
'%s Action(s) With No Effect' => array(
'Action With No Effect',
'Actions With No Effect',
),
'Some of your %s action(s) have no effect:' => array(
'One of your actions has no effect:',
'Some of your actions have no effect:',
),
'Apply remaining %d action(s)?' => array(
'Apply remaining action?',
'Apply remaining actions?',
),
'Apply %d Other Action(s)' => array(
'Apply Remaining Action',
'Apply Remaining Actions',
),
'The %s action(s) you are taking have no effect:' => array(
'The action you are taking has no effect:',
'The actions you are taking have no effect:',
),
'%s edited member(s), added %d: %s; removed %d: %s.' =>
'%s edited members, added: %3$s; removed: %5$s.',
'%s added %s member(s): %s.' => array(
array(
'%s added a member: %3$s.',
'%s added members: %3$s.',
),
),
'%s removed %s member(s): %s.' => array(
array(
'%s removed a member: %3$s.',
'%s removed members: %3$s.',
),
),
'%s edited project(s), added %s: %s; removed %s: %s.' =>
'%s edited projects, added: %3$s; removed: %5$s.',
'%s added %s project(s): %s.' => array(
array(
'%s added a project: %3$s.',
'%s added projects: %3$s.',
),
),
'%s removed %s project(s): %s.' => array(
array(
'%s removed a project: %3$s.',
'%s removed projects: %3$s.',
),
),
'%s merged %s task(s): %s.' => array(
array(
'%s merged a task: %3$s.',
'%s merged tasks: %3$s.',
),
),
'%s merged %s task(s) %s into %s.' => array(
array(
'%s merged %3$s into %4$s.',
'%s merged tasks %3$s into %4$s.',
),
),
'%s added %s voting user(s): %s.' => array(
array(
'%s added a voting user: %3$s.',
'%s added voting users: %3$s.',
),
),
'%s removed %s voting user(s): %s.' => array(
array(
'%s removed a voting user: %3$s.',
'%s removed voting users: %3$s.',
),
),
'%s added %s subtask(s): %s.' => array(
array(
'%s added a subtask: %3$s.',
'%s added subtasks: %3$s.',
),
),
'%s added %s parent task(s): %s.' => array(
array(
'%s added a parent task: %3$s.',
'%s added parent tasks: %3$s.',
),
),
'%s removed %s subtask(s): %s.' => array(
array(
'%s removed a subtask: %3$s.',
'%s removed subtasks: %3$s.',
),
),
'%s removed %s parent task(s): %s.' => array(
array(
'%s removed a parent task: %3$s.',
'%s removed parent tasks: %3$s.',
),
),
'%s added %s subtask(s) for %s: %s.' => array(
array(
'%s added a subtask for %3$s: %4$s.',
'%s added subtasks for %3$s: %4$s.',
),
),
'%s added %s parent task(s) for %s: %s.' => array(
array(
'%s added a parent task for %3$s: %4$s.',
'%s added parent tasks for %3$s: %4$s.',
),
),
'%s removed %s subtask(s) for %s: %s.' => array(
array(
'%s removed a subtask for %3$s: %4$s.',
'%s removed subtasks for %3$s: %4$s.',
),
),
'%s removed %s parent task(s) for %s: %s.' => array(
array(
'%s removed a parent task for %3$s: %4$s.',
'%s removed parent tasks for %3$s: %4$s.',
),
),
'%s edited subtask(s), added %s: %s; removed %s: %s.' =>
'%s edited subtasks, added: %3$s; removed: %5$s.',
'%s edited subtask(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited subtasks for %s, added: %4$s; removed: %6$s.',
'%s edited parent task(s), added %s: %s; removed %s: %s.' =>
'%s edited parent tasks, added: %3$s; removed: %5$s.',
'%s edited parent task(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited parent tasks for %s, added: %4$s; removed: %6$s.',
'%s edited answer(s), added %s: %s; removed %d: %s.' =>
'%s edited answers, added: %3$s; removed: %5$s.',
'%s added %s answer(s): %s.' => array(
array(
'%s added an answer: %3$s.',
'%s added answers: %3$s.',
),
),
'%s removed %s answer(s): %s.' => array(
array(
'%s removed a answer: %3$s.',
'%s removed answers: %3$s.',
),
),
'%s edited question(s), added %s: %s; removed %s: %s.' =>
'%s edited questions, added: %3$s; removed: %5$s.',
'%s added %s question(s): %s.' => array(
array(
'%s added a question: %3$s.',
'%s added questions: %3$s.',
),
),
'%s removed %s question(s): %s.' => array(
array(
'%s removed a question: %3$s.',
'%s removed questions: %3$s.',
),
),
'%s edited mock(s), added %s: %s; removed %s: %s.' =>
'%s edited mocks, added: %3$s; removed: %5$s.',
'%s added %s mock(s): %s.' => array(
array(
'%s added a mock: %3$s.',
'%s added mocks: %3$s.',
),
),
'%s removed %s mock(s): %s.' => array(
array(
'%s removed a mock: %3$s.',
'%s removed mocks: %3$s.',
),
),
'%s added %s task(s): %s.' => array(
array(
'%s added a task: %3$s.',
'%s added tasks: %3$s.',
),
),
'%s removed %s task(s): %s.' => array(
array(
'%s removed a task: %3$s.',
'%s removed tasks: %3$s.',
),
),
'%s edited file(s), added %s: %s; removed %s: %s.' =>
'%s edited files, added: %3$s; removed: %5$s.',
'%s added %s file(s): %s.' => array(
array(
'%s added a file: %3$s.',
'%s added files: %3$s.',
),
),
'%s removed %s file(s): %s.' => array(
array(
'%s removed a file: %3$s.',
'%s removed files: %3$s.',
),
),
'%s edited contributor(s), added %s: %s; removed %s: %s.' =>
'%s edited contributors, added: %3$s; removed: %5$s.',
'%s added %s contributor(s): %s.' => array(
array(
'%s added a contributor: %3$s.',
'%s added contributors: %3$s.',
),
),
'%s removed %s contributor(s): %s.' => array(
array(
'%s removed a contributor: %3$s.',
'%s removed contributors: %3$s.',
),
),
'%s edited %s reviewer(s), added %s: %s; removed %s: %s.' =>
'%s edited reviewers, added: %4$s; removed: %6$s.',
'%s edited %s reviewer(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reviewers for %3$s, added: %5$s; removed: %7$s.',
'%s added %s reviewer(s): %s.' => array(
array(
'%s added a reviewer: %3$s.',
'%s added reviewers: %3$s.',
),
),
'%s added %s reviewer(s) for %s: %s.' => array(
array(
'%s added a reviewer for %3$s: %4$s.',
'%s added reviewers for %3$s: %4$s.',
),
),
'%s removed %s reviewer(s): %s.' => array(
array(
'%s removed a reviewer: %3$s.',
'%s removed reviewers: %3$s.',
),
),
'%s removed %s reviewer(s) for %s: %s.' => array(
array(
'%s removed a reviewer for %3$s: %4$s.',
'%s removed reviewers for %3$s: %4$s.',
),
),
'%d other(s)' => array(
'1 other',
'%d others',
),
'%s edited subscriber(s), added %d: %s; removed %d: %s.' =>
'%s edited subscribers, added: %3$s; removed: %5$s.',
'%s added %d subscriber(s): %s.' => array(
array(
'%s added a subscriber: %3$s.',
'%s added subscribers: %3$s.',
),
),
'%s removed %d subscriber(s): %s.' => array(
array(
'%s removed a subscriber: %3$s.',
'%s removed subscribers: %3$s.',
),
),
'%s edited watcher(s), added %s: %s; removed %d: %s.' =>
'%s edited watchers, added: %3$s; removed: %5$s.',
'%s added %s watcher(s): %s.' => array(
array(
'%s added a watcher: %3$s.',
'%s added watchers: %3$s.',
),
),
'%s removed %s watcher(s): %s.' => array(
array(
'%s removed a watcher: %3$s.',
'%s removed watchers: %3$s.',
),
),
'%s edited participant(s), added %d: %s; removed %d: %s.' =>
'%s edited participants, added: %3$s; removed: %5$s.',
'%s added %d participant(s): %s.' => array(
array(
'%s added a participant: %3$s.',
'%s added participants: %3$s.',
),
),
'%s removed %d participant(s): %s.' => array(
array(
'%s removed a participant: %3$s.',
'%s removed participants: %3$s.',
),
),
'%s edited image(s), added %d: %s; removed %d: %s.' =>
'%s edited images, added: %3$s; removed: %5$s',
'%s added %d image(s): %s.' => array(
array(
'%s added an image: %3$s.',
'%s added images: %3$s.',
),
),
'%s removed %d image(s): %s.' => array(
array(
'%s removed an image: %3$s.',
'%s removed images: %3$s.',
),
),
'%s Line(s)' => array(
'%s Line',
'%s Lines',
),
'Indexing %d object(s) of type %s.' => array(
'Indexing %d object of type %s.',
'Indexing %d object of type %s.',
),
'Run these %d command(s):' => array(
'Run this command:',
'Run these commands:',
),
'Install these %d PHP extension(s):' => array(
'Install this PHP extension:',
'Install these PHP extensions:',
),
'The current Phabricator configuration has these %d value(s):' => array(
'The current Phabricator configuration has this value:',
'The current Phabricator configuration has these values:',
),
'The current MySQL configuration has these %d value(s):' => array(
'The current MySQL configuration has this value:',
'The current MySQL configuration has these values:',
),
'You can update these %d value(s) here:' => array(
'You can update this value here:',
'You can update these values here:',
),
'The current PHP configuration has these %d value(s):' => array(
'The current PHP configuration has this value:',
'The current PHP configuration has these values:',
),
'To update these %d value(s), edit your PHP configuration file.' => array(
'To update this %d value, edit your PHP configuration file.',
'To update these %d values, edit your PHP configuration file.',
),
'To update these %d value(s), edit your PHP configuration file, located '.
'here:' => array(
'To update this value, edit your PHP configuration file, located '.
'here:',
'To update these values, edit your PHP configuration file, located '.
'here:',
),
'PHP also loaded these %s configuration file(s):' => array(
'PHP also loaded this configuration file:',
'PHP also loaded these configuration files:',
),
'%s added %d inline comment(s).' => array(
array(
'%s added an inline comment.',
'%s added inline comments.',
),
),
'%s comment(s)' => array('%s comment', '%s comments'),
'%s rejection(s)' => array('%s rejection', '%s rejections'),
'%s update(s)' => array('%s update', '%s updates'),
'This configuration value is defined in these %d '.
'configuration source(s): %s.' => array(
'This configuration value is defined in this '.
'configuration source: %2$s.',
'This configuration value is defined in these %d '.
'configuration sources: %s.',
),
'%s Open Pull Request(s)' => array(
'%s Open Pull Request',
'%s Open Pull Requests',
),
'Stale (%s day(s))' => array(
'Stale (%s day)',
'Stale (%s days)',
),
'Old (%s day(s))' => array(
'Old (%s day)',
'Old (%s days)',
),
'%s Commit(s)' => array(
'%s Commit',
'%s Commits',
),
'%s attached %d file(s): %s.' => array(
array(
'%s attached a file: %3$s.',
'%s attached files: %3$s.',
),
),
'%s detached %d file(s): %s.' => array(
array(
'%s detached a file: %3$s.',
'%s detached files: %3$s.',
),
),
'%s changed file(s), attached %d: %s; detached %d: %s.' =>
'%s changed files, attached: %3$s; detached: %5$s.',
- '%s added %s dependencie(s): %s.' => array(
+ '%s added %s parent revision(s): %s.' => array(
array(
- '%s added a dependency: %3$s.',
- '%s added dependencies: %3$s.',
+ '%s added a parent revision: %3$s.',
+ '%s added parent revisions: %3$s.',
),
),
- '%s added %s dependencie(s) for %s: %s.' => array(
+ '%s added %s parent revision(s) for %s: %s.' => array(
array(
- '%s added a dependency for %3$s: %4$s.',
- '%s added dependencies for %3$s: %4$s.',
+ '%s added a parent revision for %3$s: %4$s.',
+ '%s added parent revisions for %3$s: %4$s.',
),
),
- '%s removed %s dependencie(s): %s.' => array(
+ '%s removed %s parent revision(s): %s.' => array(
array(
- '%s removed a dependency: %3$s.',
- '%s removed dependencies: %3$s.',
+ '%s removed a parent revision: %3$s.',
+ '%s removed parent revisions: %3$s.',
),
),
- '%s removed %s dependencie(s) for %s: %s.' => array(
+ '%s removed %s parent revision(s) for %s: %s.' => array(
array(
- '%s removed a dependency for %3$s: %4$s.',
- '%s removed dependencies for %3$s: %4$s.',
+ '%s removed a parent revision for %3$s: %4$s.',
+ '%s removed parent revisions for %3$s: %4$s.',
),
),
- '%s edited dependencie(s), added %s: %s; removed %s: %s.' => array(
- '%s edited dependencies, added: %3$s; removed: %5$s.',
+ '%s edited parent revision(s), added %s: %s; removed %s: %s.' => array(
+ '%s edited parent revisions, added: %3$s; removed: %5$s.',
),
- '%s edited dependencie(s) for %s, added %s: %s; removed %s: %s.' => array(
- '%s edited dependencies for %s, added: %3$s; removed: %5$s.',
+ '%s edited parent revision(s) for %s, '.
+ 'added %s: %s; removed %s: %s.' => array(
+ '%s edited parent revisions for %s, added: %3$s; removed: %5$s.',
),
- '%s added %s dependent revision(s): %s.' => array(
+ '%s added %s child revision(s): %s.' => array(
array(
- '%s added a dependent revision: %3$s.',
- '%s added dependent revisions: %3$s.',
+ '%s added a child revision: %3$s.',
+ '%s added child revisions: %3$s.',
),
),
- '%s added %s dependent revision(s) for %s: %s.' => array(
+ '%s added %s child revision(s) for %s: %s.' => array(
array(
- '%s added a dependent revision for %3$s: %4$s.',
- '%s added dependent revisions for %3$s: %4$s.',
+ '%s added a child revision for %3$s: %4$s.',
+ '%s added child revisions for %3$s: %4$s.',
),
),
- '%s removed %s dependent revision(s): %s.' => array(
+ '%s removed %s child revision(s): %s.' => array(
array(
- '%s removed a dependent revision: %3$s.',
- '%s removed dependent revisions: %3$s.',
+ '%s removed a child revision: %3$s.',
+ '%s removed child revisions: %3$s.',
),
),
- '%s removed %s dependent revision(s) for %s: %s.' => array(
+ '%s removed %s child revision(s) for %s: %s.' => array(
array(
- '%s removed a dependent revision for %3$s: %4$s.',
- '%s removed dependent revisions for %3$s: %4$s.',
+ '%s removed a child revision for %3$s: %4$s.',
+ '%s removed child revisions for %3$s: %4$s.',
),
),
+ '%s edited child revision(s), added %s: %s; removed %s: %s.' => array(
+ '%s edited child revisions, added: %3$s; removed: %5$s.',
+ ),
+
+ '%s edited child revision(s) for %s, '.
+ 'added %s: %s; removed %s: %s.' => array(
+ '%s edited child revisions for %s, added: %3$s; removed: %5$s.',
+ ),
+
'%s added %s commit(s): %s.' => array(
array(
'%s added a commit: %3$s.',
'%s added commits: %3$s.',
),
),
'%s removed %s commit(s): %s.' => array(
array(
'%s removed a commit: %3$s.',
'%s removed commits: %3$s.',
),
),
'%s edited commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %3$s; removed %5$s.',
'%s added %s reverted change(s): %s.' => array(
array(
'%s added a reverted change: %3$s.',
'%s added reverted changes: %3$s.',
),
),
'%s removed %s reverted change(s): %s.' => array(
array(
'%s removed a reverted change: %3$s.',
'%s removed reverted changes: %3$s.',
),
),
'%s edited reverted change(s), added %s: %s; removed %s: %s.' =>
'%s edited reverted changes, added %3$s; removed %5$s.',
'%s added %s reverted change(s) for %s: %s.' => array(
array(
'%s added a reverted change for %3$s: %4$s.',
'%s added reverted changes for %3$s: %4$s.',
),
),
'%s removed %s reverted change(s) for %s: %s.' => array(
array(
'%s removed a reverted change for %3$s: %4$s.',
'%s removed reverted changes for %3$s: %4$s.',
),
),
'%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reverted changes for %2$s, added %4$s; removed %6$s.',
'%s added %s reverting change(s): %s.' => array(
array(
'%s added a reverting change: %3$s.',
'%s added reverting changes: %3$s.',
),
),
'%s removed %s reverting change(s): %s.' => array(
array(
'%s removed a reverting change: %3$s.',
'%s removed reverting changes: %3$s.',
),
),
'%s edited reverting change(s), added %s: %s; removed %s: %s.' =>
'%s edited reverting changes, added %3$s; removed %5$s.',
'%s added %s reverting change(s) for %s: %s.' => array(
array(
'%s added a reverting change for %3$s: %4$s.',
'%s added reverting changes for %3$s: %4$s.',
),
),
'%s removed %s reverting change(s) for %s: %s.' => array(
array(
'%s removed a reverting change for %3$s: %4$s.',
'%s removed reverting changes for %3$s: %4$s.',
),
),
'%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reverting changes for %s, added %4$s; removed %6$s.',
'%s changed project member(s), added %d: %s; removed %d: %s.' =>
'%s changed project members, added %3$s; removed %5$s.',
'%s added %d project member(s): %s.' => array(
array(
'%s added a member: %3$s.',
'%s added members: %3$s.',
),
),
'%s removed %d project member(s): %s.' => array(
array(
'%s removed a member: %3$s.',
'%s removed members: %3$s.',
),
),
'%s project hashtag(s) are already used by other projects: %s.' => array(
'Project hashtag "%2$s" is already used by another project.',
'Some project hashtags are already used by other projects: %2$s.',
),
'%s changed project hashtag(s), added %d: %s; removed %d: %s.' =>
'%s changed project hashtags, added %3$s; removed %5$s.',
'Hashtags must contain at least one letter or number. %s '.
'project hashtag(s) are invalid: %s.' => array(
'Hashtags must contain at least one letter or number. The '.
'hashtag "%2$s" is not valid.',
'Hashtags must contain at least one letter or number. These '.
'hashtags are invalid: %2$s.',
),
'%s added %d project hashtag(s): %s.' => array(
array(
'%s added a hashtag: %3$s.',
'%s added hashtags: %3$s.',
),
),
'%s removed %d project hashtag(s): %s.' => array(
array(
'%s removed a hashtag: %3$s.',
'%s removed hashtags: %3$s.',
),
),
'%s changed %s hashtag(s), added %d: %s; removed %d: %s.' =>
'%s changed hashtags for %s, added %4$s; removed %6$s.',
'%s added %d %s hashtag(s): %s.' => array(
array(
'%s added a hashtag to %3$s: %4$s.',
'%s added hashtags to %3$s: %4$s.',
),
),
'%s removed %d %s hashtag(s): %s.' => array(
array(
'%s removed a hashtag from %3$s: %4$s.',
'%s removed hashtags from %3$s: %4$s.',
),
),
'%d User(s) Need Approval' => array(
'%d User Needs Approval',
'%d Users Need Approval',
),
'%s, %s line(s)' => array(
array(
'%s, %s line',
'%s, %s lines',
),
),
'%s pushed %d commit(s) to %s.' => array(
array(
'%s pushed a commit to %3$s.',
'%s pushed %d commits to %s.',
),
),
'%s commit(s)' => array(
'1 commit',
'%s commits',
),
'%s removed %s JIRA issue(s): %s.' => array(
array(
'%s removed a JIRA issue: %3$s.',
'%s removed JIRA issues: %3$s.',
),
),
'%s added %s JIRA issue(s): %s.' => array(
array(
'%s added a JIRA issue: %3$s.',
'%s added JIRA issues: %3$s.',
),
),
'%s added %s required legal document(s): %s.' => array(
array(
'%s added a required legal document: %3$s.',
'%s added required legal documents: %3$s.',
),
),
'%s updated JIRA issue(s): added %s %s; removed %d %s.' =>
'%s updated JIRA issues: added %3$s; removed %5$s.',
'%s edited %s task(s), added %s: %s; removed %s: %s.' =>
'%s edited tasks, added %4$s; removed %6$s.',
'%s added %s task(s) to %s: %s.' => array(
array(
'%s added a task to %3$s: %4$s.',
'%s added tasks to %3$s: %4$s.',
),
),
'%s removed %s task(s) from %s: %s.' => array(
array(
'%s removed a task from %3$s: %4$s.',
'%s removed tasks from %3$s: %4$s.',
),
),
'%s edited %s task(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited tasks for %3$s, added: %5$s; removed %7$s.',
'%s edited %s commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %4$s; removed %6$s.',
'%s added %s commit(s) to %s: %s.' => array(
array(
'%s added a commit to %3$s: %4$s.',
'%s added commits to %3$s: %4$s.',
),
),
'%s removed %s commit(s) from %s: %s.' => array(
array(
'%s removed a commit from %3$s: %4$s.',
'%s removed commits from %3$s: %4$s.',
),
),
'%s edited %s commit(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited commits for %3$s, added: %5$s; removed %7$s.',
'%s added %s revision(s): %s.' => array(
array(
'%s added a revision: %3$s.',
'%s added revisions: %3$s.',
),
),
'%s removed %s revision(s): %s.' => array(
array(
'%s removed a revision: %3$s.',
'%s removed revisions: %3$s.',
),
),
'%s edited %s revision(s), added %s: %s; removed %s: %s.' =>
'%s edited revisions, added %4$s; removed %6$s.',
'%s added %s revision(s) to %s: %s.' => array(
array(
'%s added a revision to %3$s: %4$s.',
'%s added revisions to %3$s: %4$s.',
),
),
'%s removed %s revision(s) from %s: %s.' => array(
array(
'%s removed a revision from %3$s: %4$s.',
'%s removed revisions from %3$s: %4$s.',
),
),
'%s edited %s revision(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited revisions for %3$s, added: %5$s; removed %7$s.',
'%s edited %s project(s), added %s: %s; removed %s: %s.' =>
'%s edited projects, added %4$s; removed %6$s.',
'%s added %s project(s) to %s: %s.' => array(
array(
'%s added a project to %3$s: %4$s.',
'%s added projects to %3$s: %4$s.',
),
),
'%s removed %s project(s) from %s: %s.' => array(
array(
'%s removed a project from %3$s: %4$s.',
'%s removed projects from %3$s: %4$s.',
),
),
'%s edited %s project(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited projects for %3$s, added: %5$s; removed %7$s.',
'%s added %s panel(s): %s.' => array(
array(
'%s added a panel: %3$s.',
'%s added panels: %3$s.',
),
),
'%s removed %s panel(s): %s.' => array(
array(
'%s removed a panel: %3$s.',
'%s removed panels: %3$s.',
),
),
'%s edited %s panel(s), added %s: %s; removed %s: %s.' =>
'%s edited panels, added %4$s; removed %6$s.',
'%s added %s dashboard(s): %s.' => array(
array(
'%s added a dashboard: %3$s.',
'%s added dashboards: %3$s.',
),
),
'%s removed %s dashboard(s): %s.' => array(
array(
'%s removed a dashboard: %3$s.',
'%s removed dashboards: %3$s.',
),
),
'%s edited %s dashboard(s), added %s: %s; removed %s: %s.' =>
'%s edited dashboards, added %4$s; removed %6$s.',
'%s added %s edge(s): %s.' => array(
array(
'%s added an edge: %3$s.',
'%s added edges: %3$s.',
),
),
'%s added %s edge(s) to %s: %s.' => array(
array(
'%s added an edge to %3$s: %4$s.',
'%s added edges to %3$s: %4$s.',
),
),
'%s removed %s edge(s): %s.' => array(
array(
'%s removed an edge: %3$s.',
'%s removed edges: %3$s.',
),
),
'%s removed %s edge(s) from %s: %s.' => array(
array(
'%s removed an edge from %3$s: %4$s.',
'%s removed edges from %3$s: %4$s.',
),
),
'%s edited edge(s), added %s: %s; removed %s: %s.' =>
'%s edited edges, added: %3$s; removed: %5$s.',
'%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited edges for %3$s, added: %5$s; removed %7$s.',
'%s added %s member(s) for %s: %s.' => array(
array(
'%s added a member for %3$s: %4$s.',
'%s added members for %3$s: %4$s.',
),
),
'%s removed %s member(s) for %s: %s.' => array(
array(
'%s removed a member for %3$s: %4$s.',
'%s removed members for %3$s: %4$s.',
),
),
'%s edited %s member(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited members for %3$s, added: %5$s; removed %7$s.',
'%d related link(s):' => array(
'Related link:',
'Related links:',
),
'You have %d unpaid invoice(s).' => array(
'You have an unpaid invoice.',
'You have unpaid invoices.',
),
'The configurations differ in the following %s way(s):' => array(
'The configurations differ:',
'The configurations differ in these ways:',
),
'Phabricator is configured with an email domain whitelist (in %s), so '.
'only users with a verified email address at one of these %s '.
'allowed domain(s) will be able to register an account: %s' => array(
array(
'Phabricator is configured with an email domain whitelist (in %s), '.
'so only users with a verified email address at %3$s will be '.
'allowed to register an account.',
'Phabricator is configured with an email domain whitelist (in %s), '.
'so only users with a verified email address at one of these '.
'allowed domains will be able to register an account: %3$s',
),
),
'Show First %d Line(s)' => array(
'Show First Line',
'Show First %d Lines',
),
"\xE2\x96\xB2 Show %d Line(s)" => array(
"\xE2\x96\xB2 Show Line",
"\xE2\x96\xB2 Show %d Lines",
),
'Show All %d Line(s)' => array(
'Show Line',
'Show All %d Lines',
),
"\xE2\x96\xBC Show %d Line(s)" => array(
"\xE2\x96\xBC Show Line",
"\xE2\x96\xBC Show %d Lines",
),
'Show Last %d Line(s)' => array(
'Show Last Line',
'Show Last %d Lines',
),
'%s marked %s inline comment(s) as done and %s inline comment(s) as '.
'not done.' => array(
array(
array(
'%s marked an inline comment as done and an inline comment '.
'as not done.',
'%s marked an inline comment as done and %3$s inline comments '.
'as not done.',
),
array(
'%s marked %s inline comments as done and an inline comment '.
'as not done.',
'%s marked %s inline comments as done and %s inline comments '.
'as done.',
),
),
),
'%s marked %s inline comment(s) as done.' => array(
array(
'%s marked an inline comment as done.',
'%s marked %s inline comments as done.',
),
),
'%s marked %s inline comment(s) as not done.' => array(
array(
'%s marked an inline comment as not done.',
'%s marked %s inline comments as not done.',
),
),
'These %s object(s) will be destroyed forever:' => array(
'This object will be destroyed forever:',
'These objects will be destroyed forever:',
),
'Are you absolutely certain you want to destroy these %s '.
'object(s)?' => array(
'Are you absolutely certain you want to destroy this object?',
'Are you absolutely certain you want to destroy these objects?',
),
'%s added %s owner(s): %s.' => array(
array(
'%s added an owner: %3$s.',
'%s added owners: %3$s.',
),
),
'%s removed %s owner(s): %s.' => array(
array(
'%s removed an owner: %3$s.',
'%s removed owners: %3$s.',
),
),
'%s changed %s package owner(s), added %s: %s; removed %s: %s.' => array(
'%s changed package owners, added: %4$s; removed: %6$s.',
),
'Found %s book(s).' => array(
'Found %s book.',
'Found %s books.',
),
'Found %s file(s)...' => array(
'Found %s file...',
'Found %s files...',
),
'Found %s file(s) in project.' => array(
'Found %s file in project.',
'Found %s files in project.',
),
'Found %s unatomized, uncached file(s).' => array(
'Found %s unatomized, uncached file.',
'Found %s unatomized, uncached files.',
),
'Found %s file(s) to atomize.' => array(
'Found %s file to atomize.',
'Found %s files to atomize.',
),
'Atomizing %s file(s).' => array(
'Atomizing %s file.',
'Atomizing %s files.',
),
'Creating %s document(s).' => array(
'Creating %s document.',
'Creating %s documents.',
),
'Deleting %s document(s).' => array(
'Deleting %s document.',
'Deleting %s documents.',
),
'Found %s obsolete atom(s) in graph.' => array(
'Found %s obsolete atom in graph.',
'Found %s obsolete atoms in graph.',
),
'Found %s new atom(s) in graph.' => array(
'Found %s new atom in graph.',
'Found %s new atoms in graph.',
),
'This call takes %s parameter(s), but only %s are documented.' => array(
array(
'This call takes %s parameter, but only %s is documented.',
'This call takes %s parameter, but only %s are documented.',
),
array(
'This call takes %s parameters, but only %s is documented.',
'This call takes %s parameters, but only %s are documented.',
),
),
'%s Passed Test(s)' => '%s Passed',
'%s Failed Test(s)' => '%s Failed',
'%s Skipped Test(s)' => '%s Skipped',
'%s Broken Test(s)' => '%s Broken',
'%s Unsound Test(s)' => '%s Unsound',
'%s Other Test(s)' => '%s Other',
'%s Bulk Task(s)' => array(
'%s Task',
'%s Tasks',
),
'%s added %s badge(s) for %s: %s.' => array(
array(
'%s added a badge for %s: %3$s.',
'%s added badges for %s: %3$s.',
),
),
'%s added %s badge(s): %s.' => array(
array(
'%s added a badge: %3$s.',
'%s added badges: %3$s.',
),
),
'%s awarded %s recipient(s) for %s: %s.' => array(
array(
'%s awarded %3$s to %4$s.',
'%s awarded %3$s to multiple recipients: %4$s.',
),
),
'%s awarded %s recipients(s): %s.' => array(
array(
'%s awarded a recipient: %3$s.',
'%s awarded multiple recipients: %3$s.',
),
),
'%s edited badge(s) for %s, added %s: %s; revoked %s: %s.' => array(
array(
'%s edited badges for %s, added %s: %s; revoked %s: %s.',
'%s edited badges for %s, added %s: %s; revoked %s: %s.',
),
),
'%s edited badge(s), added %s: %s; revoked %s: %s.' => array(
array(
'%s edited badges, added %s: %s; revoked %s: %s.',
'%s edited badges, added %s: %s; revoked %s: %s.',
),
),
'%s edited recipient(s) for %s, awarded %s: %s; revoked %s: %s.' => array(
array(
'%s edited recipients for %s, awarded %s: %s; revoked %s: %s.',
'%s edited recipients for %s, awarded %s: %s; revoked %s: %s.',
),
),
'%s edited recipient(s), awarded %s: %s; revoked %s: %s.' => array(
array(
'%s edited recipients, awarded %s: %s; revoked %s: %s.',
'%s edited recipients, awarded %s: %s; revoked %s: %s.',
),
),
'%s revoked %s badge(s) for %s: %s.' => array(
array(
'%s revoked a badge for %3$s: %4$s.',
'%s revoked multiple badges for %3$s: %4$s.',
),
),
'%s revoked %s badge(s): %s.' => array(
array(
'%s revoked a badge: %3$s.',
'%s revoked multiple badges: %3$s.',
),
),
'%s revoked %s recipient(s) for %s: %s.' => array(
array(
'%s revoked %3$s from %4$s.',
'%s revoked multiple recipients for %3$s: %4$s.',
),
),
'%s revoked %s recipients(s): %s.' => array(
array(
'%s revoked a recipient: %3$s.',
'%s revoked multiple recipients: %3$s.',
),
),
'%s automatically subscribed target(s) were not affected: %s.' => array(
'An automatically subscribed target was not affected: %2$s.',
'Automatically subscribed targets were not affected: %2$s.',
),
'Declined to resubscribe %s target(s) because they previously '.
'unsubscribed: %s.' => array(
'Delined to resubscribe a target because they previously '.
'unsubscribed: %2$s.',
'Declined to resubscribe targets because they previously '.
'unsubscribed: %2$s.',
),
'%s target(s) are not subscribed: %s.' => array(
'A target is not subscribed: %2$s.',
'Targets are not subscribed: %2$s.',
),
'%s target(s) are already subscribed: %s.' => array(
'A target is already subscribed: %2$s.',
'Targets are already subscribed: %2$s.',
),
'Added %s subscriber(s): %s.' => array(
'Added a subscriber: %2$s.',
'Added subscribers: %2$s.',
),
'Removed %s subscriber(s): %s.' => array(
'Removed a subscriber: %2$s.',
'Removed subscribers: %2$s.',
),
'Queued email to be delivered to %s target(s): %s.' => array(
'Queued email to be delivered to target: %2$s.',
'Queued email to be delivered to targets: %2$s.',
),
'Queued email to be delivered to %s target(s), ignoring their '.
'notification preferences: %s.' => array(
'Queued email to be delivered to target, ignoring notification '.
'preferences: %2$s.',
'Queued email to be delivered to targets, ignoring notification '.
'preferences: %2$s.',
),
'%s project(s) are not associated: %s.' => array(
'A project is not associated: %2$s.',
'Projects are not associated: %2$s.',
),
'%s project(s) are already associated: %s.' => array(
'A project is already associated: %2$s.',
'Projects are already associated: %2$s.',
),
'Added %s project(s): %s.' => array(
'Added a project: %2$s.',
'Added projects: %2$s.',
),
'Removed %s project(s): %s.' => array(
'Removed a project: %2$s.',
'Removed projects: %2$s.',
),
'Added %s reviewer(s): %s.' => array(
'Added a reviewer: %2$s.',
'Added reviewers: %2$s.',
),
'Added %s blocking reviewer(s): %s.' => array(
'Added a blocking reviewer: %2$s.',
'Added blocking reviewers: %2$s.',
),
'Required %s signature(s): %s.' => array(
'Required a signature: %2$s.',
'Required signatures: %2$s.',
),
'Started %s build(s): %s.' => array(
'Started a build: %2$s.',
'Started builds: %2$s.',
),
'Added %s auditor(s): %s.' => array(
'Added an auditor: %2$s.',
'Added auditors: %2$s.',
),
'%s target(s) do not have permission to see this object: %s.' => array(
'A target does not have permission to see this object: %2$s.',
'Targets do not have permission to see this object: %2$s.',
),
'This action has no effect on %s target(s): %s.' => array(
'This action has no effect on a target: %2$s.',
'This action has no effect on targets: %2$s.',
),
'Mail sent in the last %s day(s).' => array(
'Mail sent in the last day.',
'Mail sent in the last %s days.',
),
'%s Day(s)' => array(
'%s Day',
'%s Days',
),
'%s Day(s) Ago' => array(
'%s Day Ago',
'%s Days Ago',
),
'Setting retention policy for "%s" to %s day(s).' => array(
array(
'Setting retention policy for "%s" to one day.',
'Setting retention policy for "%s" to %s days.',
),
),
'Waiting %s second(s) for lease to activate.' => array(
'Waiting a second for lease to activate.',
'Waiting %s seconds for lease to activate.',
),
'%s changed %s automation blueprint(s), added %s: %s; removed %s: %s.' =>
'%s changed automation blueprints, added: %4$s; removed: %6$s.',
'%s added %s automation blueprint(s): %s.' => array(
array(
'%s added an automation blueprint: %3$s.',
'%s added automation blueprints: %3$s.',
),
),
'%s removed %s automation blueprint(s): %s.' => array(
array(
'%s removed an automation blueprint: %3$s.',
'%s removed automation blueprints: %3$s.',
),
),
'WARNING: There are %s unapproved authorization(s)!' => array(
'WARNING: There is an unapproved authorization!',
'WARNING: There are unapproved authorizations!',
),
'Found %s Open Resource(s)' => array(
'Found %s Open Resource',
'Found %s Open Resources',
),
'%s Open Resource(s) Remain' => array(
'%s Open Resource Remain',
'%s Open Resources Remain',
),
'Found %s Blueprint(s)' => array(
'Found %s Blueprint',
'Found %s Blueprints',
),
'%s Blueprint(s) Can Allocate' => array(
'%s Blueprint Can Allocate',
'%s Blueprints Can Allocate',
),
'%s Blueprint(s) Enabled' => array(
'%s Blueprint Enabled',
'%s Blueprints Enabled',
),
'%s Event(s)' => array(
'%s Event',
'%s Events',
),
'%s Unit(s)' => array(
'%s Unit',
'%s Units',
),
'QUEUEING TASKS (%s Commit(s)):' => array(
'QUEUEING TASKS (%s Commit):',
'QUEUEING TASKS (%s Commits):',
),
'Found %s total commit(s); updating...' => array(
'Found %s total commit; updating...',
'Found %s total commits; updating...',
),
'Not enough process slots to schedule the other %s '.
'repository(s) for updates yet.' => array(
'Not enough process slots to schedule the other '.'
repository for update yet.',
'Not enough process slots to schedule the other %s '.
'repositories for updates yet.',
),
'%s updated %s, added %d: %s.' =>
'%s updated %s, added: %4$s.',
'%s updated %s, removed %s: %s.' =>
'%s updated %s, removed: %4$s.',
'%s updated %s, added %s: %s; removed %s: %s.' =>
'%s updated %s, added: %4$s; removed: %6$s.',
'%s updated %s for %s, added %d: %s.' =>
'%s updated %s for %s, added: %5$s.',
'%s updated %s for %s, removed %s: %s.' =>
'%s updated %s for %s, removed: %5$s.',
'%s updated %s for %s, added %s: %s; removed %s: %s.' =>
'%s updated %s for %s, added: %5$s; removed; %7$s.',
'Permanently destroyed %s object(s).' => array(
'Permanently destroyed %s object.',
'Permanently destroyed %s objects.',
),
'%s added %s watcher(s) for %s: %s.' => array(
array(
'%s added a watcher for %3$s: %4$s.',
'%s added watchers for %3$s: %4$s.',
),
),
'%s removed %s watcher(s) for %s: %s.' => array(
array(
'%s removed a watcher for %3$s: %4$s.',
'%s removed watchers for %3$s: %4$s.',
),
),
'%s awarded this badge to %s recipient(s): %s.' => array(
array(
'%s awarded this badge to recipient: %3$s.',
'%s awarded this badge to recipients: %3$s.',
),
),
'%s revoked this badge from %s recipient(s): %s.' => array(
array(
'%s revoked this badge from recipient: %3$s.',
'%s revoked this badge from recipients: %3$s.',
),
),
'%s awarded %s to %s recipient(s): %s.' => array(
array(
array(
'%s awarded %s to recipient: %4$s.',
'%s awarded %s to recipients: %4$s.',
),
),
),
'%s revoked %s from %s recipient(s): %s.' => array(
array(
array(
'%s revoked %s from recipient: %4$s.',
'%s revoked %s from recipients: %4$s.',
),
),
),
'%s invited %s attendee(s): %s.' =>
'%s invited: %3$s.',
'%s uninvited %s attendee(s): %s.' =>
'%s uninvited: %3$s.',
'%s invited %s attendee(s): %s; uninvited %s attendee(s): %s.' =>
'%s invited: %3$s; uninvited: %5$s.',
'%s invited %s attendee(s) to %s: %s.' =>
'%s added invites for %3$s: %4$s.',
'%s uninvited %s attendee(s) to %s: %s.' =>
'%s removed invites for %3$s: %4$s.',
'%s updated the invite list for %s, invited %s: %s; uninvited %s: %s.' =>
'%s updated the invite list for %s, invited: %4$s; uninvited: %6$s.',
'Restart %s build(s)?' => array(
'Restart %s build?',
'Restart %s builds?',
),
'%s is starting in %s minute(s), at %s.' => array(
array(
'%s is starting in one minute, at %3$s.',
'%s is starting in %s minutes, at %s.',
),
),
'%s added %s auditor(s): %s.' => array(
array(
'%s added an auditor: %3$s.',
'%s added auditors: %3$s.',
),
),
'%s removed %s auditor(s): %s.' => array(
array(
'%s removed an auditor: %3$s.',
'%s removed auditors: %3$s.',
),
),
'%s edited %s auditor(s), removed %s: %s; added %s: %s.' => array(
array(
'%s edited auditors, removed: %4$s; added: %6$s.',
),
),
'%s accepted this revision as %s reviewer(s): %s.' =>
'%s accepted this revision as: %3$s.',
'%s added %s merchant manager(s): %s.' => array(
array(
'%s added a merchant manager: %3$s.',
'%s added merchant managers: %3$s.',
),
),
'%s removed %s merchant manager(s): %s.' => array(
array(
'%s removed a merchant manager: %3$s.',
'%s removed merchant managers: %3$s.',
),
),
'%s added %s account manager(s): %s.' => array(
array(
'%s added an account manager: %3$s.',
'%s added account managers: %3$s.',
),
),
'%s removed %s account manager(s): %s.' => array(
array(
'%s removed an account manager: %3$s.',
'%s removed account managers: %3$s.',
),
),
'You are about to apply a bulk edit which will affect '.
'%s object(s).' => array(
'You are about to apply a bulk edit to a single object.',
'You are about to apply a bulk edit which will affect '.
'%s objects.',
),
'Destroyed %s credential(s) of type "%s".' => array(
'Destroyed one credential of type "%2$s".',
'Destroyed %s credentials of type "%s".',
),
+ '%s notification(s) about objects which no longer exist or which '.
+ 'you can no longer see were discarded.' => array(
+ 'One notification about an object which no longer exists or which '.
+ 'you can no longer see was discarded.',
+ '%s notifications about objects which no longer exist or which '.
+ 'you can no longer see were discarded.',
+ ),
+
+ 'This draft revision will be sent for review once %s '.
+ 'build(s) pass: %s.' => array(
+ 'This draft revision will be sent for review once this build '.
+ 'passes: %2$s.',
+ 'This draft revision will be sent for review once these builds '.
+ 'pass: %2$s.',
+ ),
+
);
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 5193b1497..1784a8ce1 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,2920 +1,2940 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
* @task paging Paging
* @task order Result Ordering
* @task edgelogic Working with Edge Logic
* @task spaces Working with Spaces
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
private $applicationSearchConstraints = array();
private $internalPaging;
private $orderVector;
private $groupVector;
private $builtinOrder;
private $edgeLogicConstraints = array();
private $edgeLogicConstraintsAreValid = false;
private $spacePHIDs;
private $spaceIsArchived;
private $ngrams = array();
private $ferretEngine;
private $ferretTokens = array();
private $ferretTables = array();
private $ferretQuery;
private $ferretMetadata = array();
protected function getPageCursors(array $page) {
return array(
$this->getResultCursor(head($page)),
$this->getResultCursor(last($page)),
);
}
protected function getResultCursor($object) {
if (!is_object($object)) {
throw new Exception(
pht(
'Expected object, got "%s".',
gettype($object)));
}
return $object->getID();
}
protected function nextPage(array $page) {
// See getPagingViewer() for a description of this flag.
$this->internalPaging = true;
if ($this->beforeID !== null) {
$page = array_reverse($page, $preserve_keys = true);
list($before, $after) = $this->getPageCursors($page);
$this->beforeID = $before;
} else {
list($before, $after) = $this->getPageCursors($page);
$this->afterID = $after;
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final protected function getAfterID() {
return $this->afterID;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function getBeforeID() {
return $this->beforeID;
}
final public function getFerretMetadata() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Unable to retrieve Ferret engine metadata, this class ("%s") does '.
'not support the Ferret engine.',
get_class($this)));
}
return $this->ferretMetadata;
}
protected function loadStandardPage(PhabricatorLiskDAO $table) {
$rows = $this->loadStandardPageRows($table);
return $table->loadAllFromArray($rows);
}
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r');
return $this->loadStandardPageRowsWithConnection(
$conn,
$table->getTableName());
}
protected function loadStandardPageRowsWithConnection(
AphrontDatabaseConnection $conn,
$table_name) {
$query = $this->buildStandardPageQuery($conn, $table_name);
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $rows;
}
protected function buildStandardPageQuery(
AphrontDatabaseConnection $conn,
$table_name) {
return qsprintf(
$conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$table_name,
(string)$this->getPrimaryTableAlias(),
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
}
protected function didLoadRawRows(array $rows) {
if ($this->ferretEngine) {
foreach ($rows as $row) {
$phid = $row['phid'];
$metadata = id(new PhabricatorFerretMetadata())
->setPHID($phid)
->setEngine($this->ferretEngine)
->setRelevance(idx($row, '_ft_rank'));
$this->ferretMetadata[$phid] = $metadata;
unset($row['_ft_rank']);
}
}
return $rows;
}
/**
* Get the viewer for making cursor paging queries.
*
* NOTE: You should ONLY use this viewer to load cursor objects while
* building paging queries.
*
* Cursor paging can happen in two ways. First, the user can request a page
* like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
* can fall back to implicit paging if we filter some results out of a
* result list because the user can't see them and need to go fetch some more
* results to generate a large enough result list.
*
* In the first case, want to use the viewer's policies to load the object.
* This prevents an attacker from figuring out information about an object
* they can't see by executing queries like `/stuff/?after=33&order=name`,
* which would otherwise give them a hint about the name of the object.
* Generally, if a user can't see an object, they can't use it to page.
*
* In the second case, we need to load the object whether the user can see
* it or not, because we need to examine new results. For example, if a user
* loads `/stuff/` and we run a query for the first 100 items that they can
* see, but the first 100 rows in the database aren't visible, we need to
* be able to issue a query for the next 100 results. If we can't load the
* cursor object, we'll fail or issue the same query over and over again.
* So, generally, internal paging must bypass policy controls.
*
* This method returns the appropriate viewer, based on the context in which
* the paging is occurring.
*
* @return PhabricatorUser Viewer for executing paging queries.
*/
final protected function getPagingViewer() {
if ($this->internalPaging) {
return PhabricatorUser::getOmnipotentUser();
} else {
return $this->getViewer();
}
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
if ($this->shouldLimitResults()) {
$limit = $this->getRawResultLimit();
if ($limit) {
return qsprintf($conn_r, 'LIMIT %d', $limit);
}
}
return '';
}
protected function shouldLimitResults() {
return true;
}
final protected function didLoadResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$limit = $pager->getPageSize();
$this->setLimit($limit + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$count = count($results);
$sliced_results = $pager->sliceResults($results);
if ($sliced_results) {
list($before, $after) = $this->getPageCursors($sliced_results);
if ($pager->getBeforeID() || ($count > $limit)) {
$pager->setNextPageID($after);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
$pager->setPrevPageID($before);
}
}
return $sliced_results;
}
/**
* Return the alias this query uses to identify the primary table.
*
* Some automatic query constructions may need to be qualified with a table
* alias if the query performs joins which make column names ambiguous. If
* this is the case, return the alias for the primary table the query
* uses; generally the object table which has `id` and `phid` columns.
*
* @return string Alias for the primary table.
*/
protected function getPrimaryTableAlias() {
return null;
}
public function newResultObject() {
return null;
}
/* -( Building Query Clauses )--------------------------------------------- */
/**
* @task clauses
*/
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
return $this->formatSelectClause($parts);
}
/**
* @task clauses
*/
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = array();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$select[] = qsprintf($conn, '%T.*', $alias);
} else {
$select[] = '*';
}
$select[] = $this->buildEdgeLogicSelectClause($conn);
$select[] = $this->buildFerretSelectClause($conn);
return $select;
}
/**
* @task clauses
*/
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($joins);
}
/**
* @task clauses
*/
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = array();
$joins[] = $this->buildEdgeLogicJoinClause($conn);
$joins[] = $this->buildApplicationSearchJoinClause($conn);
$joins[] = $this->buildNgramsJoinClause($conn);
$joins[] = $this->buildFerretJoinClause($conn);
return $joins;
}
/**
* @task clauses
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($where);
}
/**
* @task clauses
*/
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
$where[] = $this->buildEdgeLogicWhereClause($conn);
$where[] = $this->buildSpacesWhereClause($conn);
$where[] = $this->buildNgramsWhereClause($conn);
$where[] = $this->buildFerretWhereClause($conn);
$where[] = $this->buildApplicationSearchWhereClause($conn);
return $where;
}
/**
* @task clauses
*/
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
$having = $this->buildHavingClauseParts($conn);
return $this->formatHavingClause($having);
}
/**
* @task clauses
*/
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = array();
$having[] = $this->buildEdgeLogicHavingClause($conn);
return $having;
}
/**
* @task clauses
*/
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if (!$this->shouldGroupQueryResultRows()) {
return '';
}
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn());
}
/**
* @task clauses
*/
protected function shouldGroupQueryResultRows() {
if ($this->shouldGroupEdgeLogicResultRows()) {
return true;
}
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return true;
}
if ($this->shouldGroupNgramResultRows()) {
return true;
}
if ($this->shouldGroupFerretResultRows()) {
return true;
}
return false;
}
/* -( Paging )------------------------------------------------------------- */
/**
* @task paging
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
if ($this->beforeID !== null) {
$cursor = $this->beforeID;
$reversed = true;
} else if ($this->afterID !== null) {
$cursor = $this->afterID;
$reversed = false;
} else {
// No paging is being applied to this query so we do not need to
// construct a paging clause.
return '';
}
$keys = array();
foreach ($vector as $order) {
$keys[] = $order->getOrderKey();
}
$value_map = $this->getPagingValueMap($cursor, $keys);
$columns = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (!array_key_exists($key, $value_map)) {
throw new Exception(
pht(
'Query "%s" failed to return a value from getPagingValueMap() '.
'for column "%s".',
get_class($this),
$key));
}
$column = $orderable[$key];
$column['value'] = $value_map[$key];
// If the vector component is reversed, we need to reverse whatever the
// order of the column is.
if ($order->getIsReversed()) {
$column['reverse'] = !idx($column, 'reverse', false);
}
$columns[] = $column;
}
return $this->buildPagingClauseFromMultipleColumns(
$conn,
$columns,
array(
'reversed' => $reversed,
));
}
/**
* @task paging
*/
protected function getPagingValueMap($cursor, array $keys) {
return array(
'id' => $cursor,
);
}
/**
* @task paging
*/
protected function loadCursorObject($cursor) {
$query = newv(get_class($this), array())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$cursor));
$this->willExecuteCursorQuery($query);
$object = $query->executeOne();
if (!$object) {
throw new Exception(
pht(
'Cursor "%s" does not identify a valid object in query "%s".',
$cursor,
get_class($this)));
}
return $object;
}
/**
* @task paging
*/
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return;
}
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
*
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
*
* To build a clause, specify the name, type, and value of each column
* to include:
*
* $this->buildPagingClauseFromMultipleColumns(
* $conn_r,
* array(
* array(
* 'table' => 't',
* 'column' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'table' => 't',
* 'column' => 'id',
* 'type' => 'int',
* 'value' => $cursor->getID(),
* ),
* ),
* array(
* 'reversed' => $is_reversed,
* ));
*
* This method will then return a composable clause for inclusion in WHERE.
*
* @param AphrontDatabaseConnection Connection query will execute on.
* @param list<map> Column description dictionaries.
* @param map Additional construction options.
* @return string Query clause.
* @task paging
*/
final protected function buildPagingClauseFromMultipleColumns(
AphrontDatabaseConnection $conn,
array $columns,
array $options) {
foreach ($columns as $column) {
PhutilTypeSpec::checkMap(
$column,
array(
'table' => 'optional string|null',
'column' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
'unique' => 'optional bool',
'null' => 'optional string|null',
));
}
PhutilTypeSpec::checkMap(
$options,
array(
'reversed' => 'optional bool',
));
$is_query_reversed = idx($options, 'reversed', false);
$clauses = array();
$accumulated = array();
$last_key = last_key($columns);
foreach ($columns as $key => $column) {
$type = $column['type'];
$null = idx($column, 'null');
if ($column['value'] === null) {
if ($null) {
$value = null;
} else {
throw new Exception(
pht(
'Column "%s" has null value, but does not specify a null '.
'behavior.',
$key));
}
} else {
switch ($type) {
case 'int':
$value = qsprintf($conn, '%d', $column['value']);
break;
case 'float':
$value = qsprintf($conn, '%f', $column['value']);
break;
case 'string':
$value = qsprintf($conn, '%s', $column['value']);
break;
default:
throw new Exception(
pht(
'Column "%s" has unknown column type "%s".',
$column['column'],
$type));
}
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$table_name = idx($column, 'table');
$column_name = $column['column'];
if ($table_name !== null) {
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
} else {
$field = qsprintf($conn, '%T', $column_name);
}
$parts = array();
if ($null) {
$can_page_if_null = ($null === 'head');
$can_page_if_nonnull = ($null === 'tail');
if ($reverse) {
$can_page_if_null = !$can_page_if_null;
$can_page_if_nonnull = !$can_page_if_nonnull;
}
$subclause = null;
if ($can_page_if_null && $value === null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NOT NULL)',
$field);
} else if ($can_page_if_nonnull && $value !== null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NULL)',
$field);
}
}
if ($value !== null) {
$parts[] = qsprintf(
$conn,
'%Q %Q %Q',
$field,
$reverse ? '>' : '<',
$value);
}
if ($parts) {
if (count($parts) > 1) {
$clause[] = '('.implode(') OR (', $parts).')';
} else {
$clause[] = head($parts);
}
}
if ($clause) {
if (count($clause) > 1) {
$clauses[] = '('.implode(') AND (', $clause).')';
} else {
$clauses[] = head($clause);
}
}
if ($value === null) {
$accumulated[] = qsprintf(
$conn,
'%Q IS NULL',
$field);
} else {
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$field,
$value);
}
}
return '('.implode(') OR (', $clauses).')';
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Select a result ordering.
*
* This is a high-level method which selects an ordering from a predefined
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
* options are user-facing and not exhaustive, but are generally convenient
* and meaningful.
*
* You can also use @{method:setOrderVector} to specify a low-level ordering
* across individual orderable columns. This offers greater control but is
* also more involved.
*
* @param string Key of a builtin order supported by this query.
* @return this
* @task order
*/
public function setOrder($order) {
$aliases = $this->getBuiltinOrderAliasMap();
if (empty($aliases[$order])) {
throw new Exception(
pht(
'Query "%s" does not support a builtin order "%s". Supported orders '.
'are: %s.',
get_class($this),
$order,
implode(', ', array_keys($aliases))));
}
$this->builtinOrder = $aliases[$order];
$this->orderVector = null;
return $this;
}
/**
* Set a grouping order to apply before primary result ordering.
*
* This allows you to preface the query order vector with additional orders,
* so you can effect "group by" queries while still respecting "order by".
*
* This is a high-level method which works alongside @{method:setOrder}. For
* lower-level control over order vectors, use @{method:setOrderVector}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setGroupVector($vector) {
$this->groupVector = $vector;
$this->orderVector = null;
return $this;
}
/**
* Get builtin orders for this class.
*
* In application UIs, we want to be able to present users with a small
* selection of meaningful order options (like "Order by Title") rather than
* an exhaustive set of column ordering options.
*
* Meaningful user-facing orders are often really orders across multiple
* columns: for example, a "title" ordering is usually implemented as a
* "title, id" ordering under the hood.
*
* Builtin orders provide a mapping from convenient, understandable
* user-facing orders to implementations.
*
* A builtin order should provide these keys:
*
* - `vector` (`list<string>`): The actual order vector to use.
* - `name` (`string`): Human-readable order name.
*
* @return map<string, wild> Map from builtin order keys to specification.
* @task order
*/
public function getBuiltinOrders() {
$orders = array(
'newest' => array(
'vector' => array('id'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-id'),
'name' => pht('Creation (Oldest First)'),
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$legacy_key = 'custom:'.$field->getFieldKey();
$modern_key = $field->getModernFieldKey();
$orders[$modern_key] = array(
'vector' => array($modern_key, 'id'),
'name' => $field->getFieldName(),
'aliases' => array($legacy_key),
);
$orders['-'.$modern_key] = array(
'vector' => array('-'.$modern_key, '-id'),
'name' => pht('%s (Reversed)', $field->getFieldName()),
);
}
}
if ($this->supportsFerretEngine()) {
$orders['relevance'] = array(
'vector' => array('rank', 'fulltext-modified', 'id'),
'name' => pht('Relevance'),
);
}
return $orders;
}
public function getBuiltinOrderAliasMap() {
$orders = $this->getBuiltinOrders();
$map = array();
foreach ($orders as $key => $order) {
$keys = array();
$keys[] = $key;
foreach (idx($order, 'aliases', array()) as $alias) {
$keys[] = $alias;
}
foreach ($keys as $alias) {
if (isset($map[$alias])) {
throw new Exception(
pht(
'Two builtin orders ("%s" and "%s") define the same key or '.
'alias ("%s"). Each order alias and key must be unique and '.
'identify a single order.',
$key,
$map[$alias],
$alias));
}
$map[$alias] = $key;
}
}
return $map;
}
/**
* Set a low-level column ordering.
*
* This is a low-level method which offers granular control over column
* ordering. In most cases, applications can more easily use
* @{method:setOrder} to choose a high-level builtin order.
*
* To set an order vector, specify a list of order keys as provided by
* @{method:getOrderableColumns}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setOrderVector($vector) {
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
$orderable = $this->getOrderableColumns();
// Make sure that all the components identify valid columns.
$unique = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (empty($orderable[$key])) {
$valid = implode(', ', array_keys($orderable));
throw new Exception(
pht(
'This query ("%s") does not support sorting by order key "%s". '.
'Supported orders are: %s.',
get_class($this),
$key,
$valid));
}
$unique[$key] = idx($orderable[$key], 'unique', false);
}
// Make sure that the last column is unique so that this is a strong
// ordering which can be used for paging.
$last = last($unique);
if ($last !== true) {
throw new Exception(
pht(
'Order vector "%s" is invalid: the last column in an order must '.
'be a column with unique values, but "%s" is not unique.',
$vector->getAsString(),
last_key($unique)));
}
// Make sure that other columns are not unique; an ordering like "id, name"
// does not make sense because only "id" can ever have an effect.
array_pop($unique);
foreach ($unique as $key => $is_unique) {
if ($is_unique) {
throw new Exception(
pht(
'Order vector "%s" is invalid: only the last column in an order '.
'may be unique, but "%s" is a unique column and not the last '.
'column in the order.',
$vector->getAsString(),
$key));
}
}
$this->orderVector = $vector;
return $this;
}
/**
* Get the effective order vector.
*
* @return PhabricatorQueryOrderVector Effective vector.
* @task order
*/
protected function getOrderVector() {
if (!$this->orderVector) {
if ($this->builtinOrder !== null) {
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
$vector = $builtin_order['vector'];
} else {
$vector = $this->getDefaultOrderVector();
}
if ($this->groupVector) {
$group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
$group->appendVector($vector);
$vector = $group;
}
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
// We call setOrderVector() here to apply checks to the default vector.
// This catches any errors in the implementation.
$this->setOrderVector($vector);
}
return $this->orderVector;
}
/**
* @task order
*/
protected function getDefaultOrderVector() {
return array('id');
}
/**
* @task order
*/
public function getOrderableColumns() {
$cache = PhabricatorCaches::getRequestCache();
$class = get_class($this);
$cache_key = 'query.orderablecolumns.'.$class;
$columns = $cache->getKey($cache_key);
if ($columns !== null) {
return $columns;
}
$columns = array(
'id' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'id',
'reverse' => false,
'type' => 'int',
'unique' => true,
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$digest = $field->getFieldIndex();
$key = $field->getModernFieldKey();
$columns[$key] = array(
'table' => 'appsearch_order_'.$digest,
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
if ($this->supportsFerretEngine()) {
$columns['rank'] = array(
'table' => null,
'column' => '_ft_rank',
'type' => 'int',
);
$columns['fulltext-created'] = array(
'table' => 'ft_doc',
'column' => 'epochCreated',
'type' => 'int',
);
$columns['fulltext-modified'] = array(
'table' => 'ft_doc',
'column' => 'epochModified',
'type' => 'int',
);
}
$cache->setKey($cache_key, $columns);
return $columns;
}
/**
* @task order
*/
final protected function buildOrderClause(
AphrontDatabaseConnection $conn,
$for_union = false) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
$parts[] = $part;
}
return $this->formatOrderClause($conn, $parts, $for_union);
}
/**
* @task order
*/
protected function formatOrderClause(
AphrontDatabaseConnection $conn,
array $parts,
$for_union = false) {
$is_query_reversed = false;
if ($this->getBeforeID()) {
$is_query_reversed = !$is_query_reversed;
}
$sql = array();
foreach ($parts as $key => $part) {
$is_column_reversed = !empty($part['reverse']);
$descending = true;
if ($is_query_reversed) {
$descending = !$descending;
}
if ($is_column_reversed) {
$descending = !$descending;
}
$table = idx($part, 'table');
// When we're building an ORDER BY clause for a sequence of UNION
// statements, we can't refer to tables from the subqueries.
if ($for_union) {
$table = null;
}
$column = $part['column'];
if ($table !== null) {
$field = qsprintf($conn, '%T.%T', $table, $column);
} else {
$field = qsprintf($conn, '%T', $column);
}
$null = idx($part, 'null');
if ($null) {
switch ($null) {
case 'head':
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
break;
case 'tail':
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
break;
default:
throw new Exception(
pht(
'NULL value "%s" is invalid. Valid values are "head" and '.
'"tail".',
$null));
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
}
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $field);
}
}
return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql));
}
/* -( Application Search )------------------------------------------------- */
/**
* Constrain the query with an ApplicationSearch index, requiring field values
* contain at least one of the values in a set.
*
* This constraint can build the most common types of queries, like:
*
* - Find users with shirt sizes "X" or "XL".
* - Find shoes with size "13".
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param string|list<string> One or more values to filter by.
* @return this
* @task appsearch
*/
public function withApplicationSearchContainsConstraint(
PhabricatorCustomFieldIndexStorage $index,
$value) {
$values = (array)$value;
$data_values = array();
$constraint_values = array();
foreach ($values as $value) {
if ($value instanceof PhabricatorQueryConstraint) {
$constraint_values[] = $value;
} else {
$data_values[] = $value;
}
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => '=',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => $values,
'data' => $data_values,
'constraints' => $constraint_values,
);
return $this;
}
/**
* Constrain the query with an ApplicationSearch index, requiring values
* exist in a given range.
*
* This constraint is useful for expressing date ranges:
*
* - Find events between July 1st and July 7th.
*
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
* either end of the range will leave that end of the constraint open.
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param int|null Minimum permissible value, inclusive.
* @param int|null Maximum permissible value, inclusive.
* @return this
* @task appsearch
*/
public function withApplicationSearchRangeConstraint(
PhabricatorCustomFieldIndexStorage $index,
$min,
$max) {
$index_type = $index->getIndexValueType();
if ($index_type != 'int') {
throw new Exception(
pht(
'Attempting to apply a range constraint to a field with index type '.
'"%s", expected type "%s".',
$index_type,
'int'));
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => 'range',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => array($min, $max),
);
return $this;
}
/**
* Get the name of the query's primary object PHID column, for constructing
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
* be something more exotic.
*
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
* a table alias.
*
* @return string Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn() {
if ($this->getPrimaryTableAlias()) {
$prefix = $this->getPrimaryTableAlias().'.';
} else {
$prefix = '';
}
return $prefix.'phid';
}
/**
* Determine if the JOINs built by ApplicationSearch might cause each primary
* object to return multiple result rows. Generally, this means the query
* needs an extra GROUP BY clause.
*
* @return bool True if the query may return multiple rows for each object.
* @task appsearch
*/
protected function getApplicationSearchMayJoinMultipleRows() {
foreach ($this->applicationSearchConstraints as $constraint) {
$type = $constraint['type'];
$value = $constraint['value'];
$cond = $constraint['cond'];
switch ($cond) {
case '=':
switch ($type) {
case 'string':
case 'int':
if (count($value) > 1) {
return true;
}
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
break;
case 'range':
// NOTE: It's possible to write a custom field where multiple rows
// match a range constraint, but we don't currently ship any in the
// upstream and I can't immediately come up with cases where this
// would make sense.
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
return false;
}
/**
* Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Group clause.
* @task appsearch
*/
protected function buildApplicationSearchGroupClause(
AphrontDatabaseConnection $conn_r) {
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return qsprintf(
$conn_r,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn());
} else {
return '';
}
}
/**
* Construct a JOIN clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Join clause.
* @task appsearch
*/
protected function buildApplicationSearchJoinClause(
AphrontDatabaseConnection $conn) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
$alias = $constraint['alias'];
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
switch ($cond) {
case '=':
// Figure out whether we need to do a LEFT JOIN or not. We need to
// LEFT JOIN if we're going to select "IS NULL" rows.
$join_type = 'JOIN';
foreach ($constraint['constraints'] as $query_constraint) {
$op = $query_constraint->getOperator();
if ($op === PhabricatorQueryConstraint::OPERATOR_NULL) {
$join_type = 'LEFT JOIN';
break;
}
}
$joins[] = qsprintf(
$conn,
'%Q %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$join_type,
$table,
$alias,
$alias,
$phid_column,
$alias,
$index);
break;
case 'range':
list($min, $max) = $constraint['value'];
if (($min === null) && ($max === null)) {
// If there's no actual range constraint, just move on.
break;
}
if ($min === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
return implode(' ', $joins);
}
/**
* Construct a WHERE clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return list<string> Where clause parts.
* @task appsearch
*/
protected function buildApplicationSearchWhereClause(
AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$alias = $constraint['alias'];
$cond = $constraint['cond'];
$type = $constraint['type'];
$data_values = $constraint['data'];
$constraint_values = $constraint['constraints'];
$constraint_parts = array();
switch ($cond) {
case '=':
if ($data_values) {
switch ($type) {
case 'string':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ls)',
$alias,
$data_values);
break;
case 'int':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ld)',
$alias,
$data_values);
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
}
if ($constraint_values) {
foreach ($constraint_values as $value) {
$op = $value->getOperator();
switch ($op) {
case PhabricatorQueryConstraint::OPERATOR_NULL:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_ANY:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NOT NULL',
$alias);
break;
default:
throw new Exception(
pht(
'No support for applying operator "%s" against '.
'index of type "%s".',
$op,
$type));
}
}
}
if ($constraint_parts) {
$where[] = '('.implode(') OR (', $constraint_parts).')';
}
break;
}
}
return $where;
}
/* -( Integration with CustomField )--------------------------------------- */
/**
* @task customfield
*/
protected function getPagingValueMapForCustomFields(
PhabricatorCustomFieldInterface $object) {
// We have to get the current field values on the cursor object.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->getViewer());
$fields->readFieldsFromStorage($object);
$map = array();
foreach ($fields->getFields() as $field) {
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
}
return $map;
}
/**
* @task customfield
*/
protected function isCustomFieldOrderKey($key) {
$prefix = 'custom:';
return !strncmp($key, $prefix, strlen($prefix));
}
/* -( Ferret )------------------------------------------------------------- */
public function supportsFerretEngine() {
$object = $this->newResultObject();
return ($object instanceof PhabricatorFerretInterface);
}
public function withFerretQuery(
PhabricatorFerretEngine $engine,
PhabricatorSavedQuery $query) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
$this->ferretEngine = $engine;
$this->ferretQuery = $query;
return $this;
}
public function getFerretTokens() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
return $this->ferretTokens;
}
public function withFerretConstraint(
PhabricatorFerretEngine $engine,
array $fulltext_tokens) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
if ($this->ferretEngine) {
throw new Exception(
pht(
'Query may not have multiple fulltext constraints.'));
}
if (!$fulltext_tokens) {
return $this;
}
$this->ferretEngine = $engine;
$this->ferretTokens = $fulltext_tokens;
$current_function = $engine->getDefaultFunctionKey();
$table_map = array();
$idx = 1;
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$raw_field = $engine->getFieldForFunction($function);
if (!isset($table_map[$function])) {
$alias = 'ftfield_'.$idx++;
$table_map[$function] = array(
'alias' => $alias,
'key' => $raw_field,
);
}
$current_function = $function;
}
// Join the title field separately so we can rank results.
$table_map['rank'] = array(
'alias' => 'ft_rank',
'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE,
);
$this->ferretTables = $table_map;
return $this;
}
protected function buildFerretSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
if (!$this->supportsFerretEngine()) {
return $select;
}
$vector = $this->getOrderVector();
if (!$vector->containsKey('rank')) {
// We only need to SELECT the virtual "_ft_rank" column if we're
// actually sorting the results by rank.
return $select;
}
if (!$this->ferretEngine) {
$select[] = '0 _ft_rank';
return $select;
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$table_alias = 'ft_rank';
$parts = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
if ($raw_token->getOperator() == $op_not) {
// Ignore "not" terms when ranking, since they aren't useful.
continue;
}
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
if ($is_substring) {
$parts[] = qsprintf(
$conn,
'IF(%T.rawCorpus LIKE %~, 2, 0)',
$table_alias,
$value);
continue;
}
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
$parts[] = qsprintf(
$conn,
'IF(%T.termCorpus LIKE %~, 2, 0)',
$table_alias,
$term_value);
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$parts[] = qsprintf(
$conn,
'IF(%T.normalCorpus LIKE %~, 1, 0)',
$table_alias,
$stem_value);
}
}
$parts[] = '0';
$select[] = qsprintf(
$conn,
'%Q _ft_rank',
implode(' + ', $parts));
return $select;
}
protected function buildFerretJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$ngram_table = $engine->getNgramsTableName();
$flat = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
// If this is a negated term like "-pomegranate", don't join the ngram
// table since we aren't looking for documents with this term. (We could
// LEFT JOIN the table and require a NULL row, but this is probably more
// trouble than it's worth.)
if ($raw_token->getOperator() == $op_not) {
continue;
}
$value = $raw_token->getValue();
$length = count(phutil_utf8v($value));
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If the user specified a substring query for a substring which is
// shorter than the ngram length, we can't use the ngram index, so
// don't do a join. We'll fall back to just doing LIKE on the full
// corpus.
if ($is_substring) {
if ($length < 3) {
continue;
}
}
if ($raw_token->isQuoted()) {
$is_stemmed = false;
} else {
$is_stemmed = true;
}
if ($is_substring) {
$ngrams = $engine->getSubstringNgramsFromString($value);
} else {
$terms_value = $engine->newTermsCorpus($value);
$ngrams = $engine->getTermNgramsFromString($terms_value);
// If this is a stemmed term, only look for ngrams present in both the
// unstemmed and stemmed variations.
if ($is_stemmed) {
// Trim the boundary space characters so the stemmer recognizes this
// is (or, at least, may be) a normal word and activates.
$terms_value = trim($terms_value, ' ');
$stem_value = $stemmer->stemToken($terms_value);
$stem_ngrams = $engine->getTermNgramsFromString($stem_value);
$ngrams = array_intersect($ngrams, $stem_ngrams);
}
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $ngram_table,
'ngram' => $ngram,
);
}
}
// Remove common ngrams, like "the", which occur too frequently in
// documents to be useful in constraining the query. The best ngrams
// are obscure sequences which occur in very few documents.
if ($flat) {
$common_ngrams = queryfx_all(
$conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)',
$engine->getCommonNgramsTableName(),
ipull($flat, 'ngram'));
$common_ngrams = ipull($common_ngrams, 'ngram', 'ngram');
foreach ($flat as $key => $spec) {
$ngram = $spec['ngram'];
if (isset($common_ngrams[$ngram])) {
unset($flat[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trim_ngram = rtrim($ngram, ' ');
if (isset($common_ngrams[$trim_ngram])) {
unset($flat[$key]);
continue;
}
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$phid_column = qsprintf($conn, '%T.%T', $alias, 'phid');
} else {
$phid_column = qsprintf($conn, '%T', 'phid');
}
$document_table = $engine->getDocumentTableName();
$field_table = $engine->getFieldTableName();
$joins = array();
$joins[] = qsprintf(
$conn,
'JOIN %T ft_doc ON ft_doc.objectPHID = %Q',
$document_table,
$phid_column);
$idx = 1;
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$alias = 'ftngram_'.$idx++;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.documentID = ft_doc.id AND %T.ngram = %s',
$table,
$alias,
$alias,
$alias,
$ngram);
}
foreach ($this->ferretTables as $table) {
$alias = $table['alias'];
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON ft_doc.id = %T.documentID
AND %T.fieldKey = %s',
$field_table,
$alias,
$alias,
$alias,
$table['key']);
}
return $joins;
}
protected function buildFerretWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$table_map = $this->ferretTables;
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
+ $op_exact = PhutilSearchQueryCompiler::OPERATOR_EXACT;
$where = array();
$current_function = 'all';
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$current_function = $function;
$table_alias = $table_map[$function]['alias'];
$is_not = ($raw_token->getOperator() == $op_not);
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
+ // If we're doing exact search, just test the raw corpus.
+ $is_exact = ($raw_token->getOperator() == $op_exact);
+ if ($is_exact) {
+ if ($is_not) {
+ $where[] = qsprintf(
+ $conn,
+ '(%T.rawCorpus != %s)',
+ $table_alias,
+ $value);
+ } else {
+ $where[] = qsprintf(
+ $conn,
+ '(%T.rawCorpus = %s)',
+ $table_alias,
+ $value);
+ }
+ continue;
+ }
+
// If we're doing substring search, we just match against the raw corpus
// and we're done.
if ($is_substring) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus NOT LIKE %~)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~)',
$table_alias,
$value);
}
continue;
}
// Otherwise, we need to match against the term corpus and the normal
// corpus, so that searching for "raw" does not find "strawberry".
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
// Never stem negated queries, since this can exclude results users
// did not mean to exclude and generally confuse things.
if ($is_not) {
$is_stemmed = false;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
if ($is_not) {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus NOT LIKE %~)',
$table_alias,
$term_value);
} else {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus LIKE %~)',
$table_alias,
$term_value);
}
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$term_constraints[] = qsprintf(
$conn,
'(%T.normalCorpus LIKE %~)',
$table_alias,
$stem_value);
}
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%Q)',
implode(' AND ', $term_constraints));
} else if ($is_quoted) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~ AND (%Q))',
$table_alias,
$value,
implode(' OR ', $term_constraints));
} else {
$where[] = qsprintf(
$conn,
'(%Q)',
implode(' OR ', $term_constraints));
}
}
if ($this->ferretQuery) {
$query = $this->ferretQuery;
$author_phids = $query->getParameter('authorPHIDs');
if ($author_phids) {
$where[] = qsprintf(
$conn,
'ft_doc.authorPHID IN (%Ls)',
$author_phids);
}
$with_unowned = $query->getParameter('withUnowned');
$with_any = $query->getParameter('withAnyOwner');
if ($with_any && $with_unowned) {
throw new PhabricatorEmptyQueryException(
pht(
'This query matches only unowned documents owned by anyone, '.
'which is impossible.'));
}
$owner_phids = $query->getParameter('ownerPHIDs');
if ($owner_phids && !$with_any) {
if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls) OR ft_doc.ownerPHID IS NULL',
$owner_phids);
} else {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls)',
$owner_phids);
}
} else if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NULL');
}
if ($with_any) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NOT NULL');
}
$rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
$statuses = $query->getParameter('statuses');
$is_closed = null;
if ($statuses) {
$statuses = array_fuse($statuses);
if (count($statuses) == 1) {
if (isset($statuses[$rel_open])) {
$is_closed = 0;
} else {
$is_closed = 1;
}
}
}
if ($is_closed !== null) {
$where[] = qsprintf(
$conn,
'ft_doc.isClosed = %d',
$is_closed);
}
}
return $where;
}
protected function shouldGroupFerretResultRows() {
return (bool)$this->ferretTokens;
}
/* -( Ngrams )------------------------------------------------------------- */
protected function withNgramsConstraint(
PhabricatorSearchNgrams $index,
$value) {
if (strlen($value)) {
$this->ngrams[] = array(
'index' => $index,
'value' => $value,
'length' => count(phutil_utf8v($value)),
);
}
return $this;
}
protected function buildNgramsJoinClause(AphrontDatabaseConnection $conn) {
$flat = array();
foreach ($this->ngrams as $spec) {
$index = $spec['index'];
$value = $spec['value'];
$length = $spec['length'];
if ($length >= 3) {
$ngrams = $index->getNgramsFromString($value, 'query');
$prefix = false;
} else if ($length == 2) {
$ngrams = $index->getNgramsFromString($value, 'prefix');
$prefix = false;
} else {
$ngrams = array(' '.$value);
$prefix = true;
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $index->getTableName(),
'ngram' => $ngram,
'prefix' => $prefix,
);
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$id_column = qsprintf($conn, '%T.%T', $alias, 'id');
} else {
$id_column = qsprintf($conn, '%T', 'id');
}
$idx = 1;
$joins = array();
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$prefix = $spec['prefix'];
$alias = 'ngm'.$idx++;
if ($prefix) {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram LIKE %>',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
} else {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram = %s',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
}
}
return $joins;
}
protected function buildNgramsWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->ngrams as $ngram) {
$index = $ngram['index'];
$value = $ngram['value'];
$column = $index->getColumnName();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$column = qsprintf($conn, '%T.%T', $alias, $column);
} else {
$column = qsprintf($conn, '%T', $column);
}
$tokens = $index->tokenizeString($value);
foreach ($tokens as $token) {
$where[] = qsprintf(
$conn,
'%Q LIKE %~',
$column,
$token);
}
}
return $where;
}
protected function shouldGroupNgramResultRows() {
return (bool)$this->ngrams;
}
/* -( Edge Logic )--------------------------------------------------------- */
/**
* Convenience method for specifying edge logic constraints with a list of
* PHIDs.
*
* @param const Edge constant.
* @param const Constraint operator.
* @param list<phid> List of PHIDs.
* @return this
* @task edgelogic
*/
public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
$constraints = array();
foreach ($phids as $phid) {
$constraints[] = new PhabricatorQueryConstraint($operator, $phid);
}
return $this->withEdgeLogicConstraints($edge_type, $constraints);
}
/**
* @return this
* @task edgelogic
*/
public function withEdgeLogicConstraints($edge_type, array $constraints) {
assert_instances_of($constraints, 'PhabricatorQueryConstraint');
$constraints = mgroup($constraints, 'getOperator');
foreach ($constraints as $operator => $list) {
foreach ($list as $item) {
$this->edgeLogicConstraints[$edge_type][$operator][] = $item;
}
}
$this->edgeLogicConstraintsAreValid = false;
return $this;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
$this->validateEdgeLogicConstraints();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(%T.dst)) %T',
$alias,
$this->buildEdgeLogicTableAliasCount($alias));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// This is tricky. We have a query which specifies multiple
// projects, each of which may have an arbitrarily large number
// of descendants.
// Suppose the projects are "Engineering" and "Operations", and
// "Engineering" has subprojects X, Y and Z.
// We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row
// is not part of Engineering at all, or some number other than
// 0 if it is.
// Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and
// any other value to an index (say, 1) for the ancestor.
// We build these up for every ancestor, then use `COALESCE(...)`
// to select the non-null one, giving us an ancestor which this
// row is a member of.
// From there, we use `COUNT(DISTINCT(...))` to make sure that
// each result row is a member of all ancestors.
if (count($list) > 1) {
$idx = 1;
$parts = array();
foreach ($list as $constraint) {
$parts[] = qsprintf(
$conn,
'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)',
$alias,
(array)$constraint->getValue(),
$idx++);
}
$parts = implode(', ', $parts);
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(COALESCE(%Q))) %T',
$parts,
$this->buildEdgeLogicTableAliasAncestor($alias));
}
break;
default:
break;
}
}
}
return $select;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$joins = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
// If we're going to process an only() operator, build a list of the
// acceptable set of PHIDs first. We'll only match results which have
// no edges to any other PHIDs.
$all_phids = array();
if (isset($constraints[PhabricatorQueryConstraint::OPERATOR_ONLY])) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$all_phids[$v] = $v;
}
}
break;
}
}
}
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
$phids = array();
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$phids[$v] = $v;
}
}
$phids = array_keys($phids);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
// If we're including results with no matches, we have to degrade
// this to a LEFT join. We'll use WHERE to select matching rows
// later.
if ($has_null) {
$join_type = 'LEFT';
} else {
$join_type = '';
}
$joins[] = qsprintf(
$conn,
'%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$join_type,
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type);
break;
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst NOT IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$all_phids);
break;
}
}
}
return $joins;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$full = array();
$null = array();
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$full[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if ($has_null) {
$full[] = qsprintf(
$conn,
'%T.dst IS NOT NULL',
$alias);
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$null[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
}
}
if ($full && $null) {
$full = $this->formatWhereSubclause($full);
$null = $this->formatWhereSubclause($null);
$where[] = qsprintf($conn, '(%Q OR %Q)', $full, $null);
} else if ($full) {
foreach ($full as $condition) {
$where[] = $condition;
}
} else if ($null) {
foreach ($null as $condition) {
$where[] = $condition;
}
}
}
return $where;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
$having = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasCount($alias),
count($list));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasAncestor($alias),
count($list));
}
break;
}
}
}
return $having;
}
/**
* @task edgelogic
*/
public function shouldGroupEdgeLogicResultRows() {
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if (count($list) > 1) {
return true;
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// NOTE: We must always group query results rows when using an
// "ANCESTOR" operator because a single task may be related to
// two different descendants of a particular ancestor. For
// discussion, see T12753.
return true;
case PhabricatorQueryConstraint::OPERATOR_NULL:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
return true;
}
}
}
return false;
}
/**
* @task edgelogic
*/
private function getEdgeLogicTableAlias($operator, $type) {
return 'edgelogic_'.$operator.'_'.$type;
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasCount($alias) {
return $alias.'_count';
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasAncestor($alias) {
return $alias.'_ancestor';
}
/**
* Select certain edge logic constraint values.
*
* @task edgelogic
*/
protected function getEdgeLogicValues(
array $edge_types,
array $operators) {
$values = array();
$constraint_lists = $this->edgeLogicConstraints;
if ($edge_types) {
$constraint_lists = array_select_keys($constraint_lists, $edge_types);
}
foreach ($constraint_lists as $type => $constraints) {
if ($operators) {
$constraints = array_select_keys($constraints, $operators);
}
foreach ($constraints as $operator => $list) {
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$values[] = $v;
}
}
}
}
return $values;
}
/**
* Validate edge logic constraints for the query.
*
* @return this
* @task edgelogic
*/
private function validateEdgeLogicConstraints() {
if ($this->edgeLogicConstraintsAreValid) {
return $this;
}
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_EMPTY:
throw new PhabricatorEmptyQueryException(
pht('This query specifies an empty constraint.'));
}
}
}
// This should probably be more modular, eventually, but we only do
// project-based edge logic today.
$project_phids = $this->getEdgeLogicValues(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
),
array(
PhabricatorQueryConstraint::OPERATOR_AND,
PhabricatorQueryConstraint::OPERATOR_OR,
PhabricatorQueryConstraint::OPERATOR_NOT,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($project_phids as $phid) {
if (empty($projects[$phid])) {
throw new PhabricatorEmptyQueryException(
pht(
'This query is constrained by a project you do not have '.
'permission to see.'));
}
}
}
$op_and = PhabricatorQueryConstraint::OPERATOR_AND;
$op_or = PhabricatorQueryConstraint::OPERATOR_OR;
$op_ancestor = PhabricatorQueryConstraint::OPERATOR_ANCESTOR;
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ONLY:
if (count($list) > 1) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only() more than once.'));
}
$have_and = idx($constraints, $op_and);
$have_or = idx($constraints, $op_or);
$have_ancestor = idx($constraints, $op_ancestor);
if (!$have_and && !$have_or && !$have_ancestor) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only(), but no other constraints '.
'which it can apply to.'));
}
break;
}
}
}
$this->edgeLogicConstraintsAreValid = true;
return $this;
}
/* -( Spaces )------------------------------------------------------------- */
/**
* Constrain the query to return results from only specific Spaces.
*
* Pass a list of Space PHIDs, or `null` to represent the default space. Only
* results in those Spaces will be returned.
*
* Queries are always constrained to include only results from spaces the
* viewer has access to.
*
* @param list<phid|null>
* @task spaces
*/
public function withSpacePHIDs(array $space_phids) {
$object = $this->newResultObject();
if (!$object) {
throw new Exception(
pht(
'This query (of class "%s") does not implement newResultObject(), '.
'but must implement this method to enable support for Spaces.',
get_class($this)));
}
if (!($object instanceof PhabricatorSpacesInterface)) {
throw new Exception(
pht(
'This query (of class "%s") returned an object of class "%s" from '.
'getNewResultObject(), but it does not implement the required '.
'interface ("%s"). Objects must implement this interface to enable '.
'Spaces support.',
get_class($this),
get_class($object),
'PhabricatorSpacesInterface'));
}
$this->spacePHIDs = $space_phids;
return $this;
}
public function withSpaceIsArchived($archived) {
$this->spaceIsArchived = $archived;
return $this;
}
/**
* Constrain the query to include only results in valid Spaces.
*
* This method builds part of a WHERE clause which considers the spaces the
* viewer has access to see with any explicit constraint on spaces added by
* @{method:withSpacePHIDs}.
*
* @param AphrontDatabaseConnection Database connection.
* @return string Part of a WHERE clause.
* @task spaces
*/
private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
$object = $this->newResultObject();
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getViewer();
// If we have an omnipotent viewer and no formal space constraints, don't
// emit a clause. This primarily enables older migrations to run cleanly,
// without fataling because they try to match a `spacePHID` column which
// does not exist yet. See T8743, T8746.
if ($viewer->isOmnipotent()) {
if ($this->spaceIsArchived === null && $this->spacePHIDs === null) {
return null;
}
}
$space_phids = array();
$include_null = false;
$all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
if (!$all) {
// If there are no spaces at all, implicitly give the viewer access to
// the default space.
$include_null = true;
} else {
// Otherwise, give them access to the spaces they have permission to
// see.
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$viewer);
foreach ($viewer_spaces as $viewer_space) {
if ($this->spaceIsArchived !== null) {
if ($viewer_space->getIsArchived() != $this->spaceIsArchived) {
continue;
}
}
$phid = $viewer_space->getPHID();
$space_phids[$phid] = $phid;
if ($viewer_space->getIsDefaultNamespace()) {
$include_null = true;
}
}
}
// If we have additional explicit constraints, evaluate them now.
if ($this->spacePHIDs !== null) {
$explicit = array();
$explicit_null = false;
foreach ($this->spacePHIDs as $phid) {
if ($phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($all, $phid);
}
if ($space) {
$phid = $space->getPHID();
$explicit[$phid] = $phid;
if ($space->getIsDefaultNamespace()) {
$explicit_null = true;
}
}
}
// If the viewer can see the default space but it isn't on the explicit
// list of spaces to query, don't match it.
if ($include_null && !$explicit_null) {
$include_null = false;
}
// Include only the spaces common to the viewer and the constraints.
$space_phids = array_intersect_key($space_phids, $explicit);
}
if (!$space_phids && !$include_null) {
if ($this->spacePHIDs === null) {
throw new PhabricatorEmptyQueryException(
pht('You do not have access to any spaces.'));
} else {
throw new PhabricatorEmptyQueryException(
pht(
'You do not have access to any of the spaces this query '.
'is constrained to.'));
}
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$col = qsprintf($conn, '%T.spacePHID', $alias);
} else {
$col = 'spacePHID';
}
if ($space_phids && $include_null) {
return qsprintf(
$conn,
'(%Q IN (%Ls) OR %Q IS NULL)',
$col,
$space_phids,
$col);
} else if ($space_phids) {
return qsprintf(
$conn,
'%Q IN (%Ls)',
$col,
$space_phids);
} else {
return qsprintf(
$conn,
'%Q IS NULL',
$col);
}
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
index 743ca9b75..b3b324e95 100644
--- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
+++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
@@ -1,319 +1,332 @@
<?php
/**
* @task config Configuring Storage
*/
abstract class PhabricatorLiskDAO extends LiskDAO {
private static $namespaceStack = array();
const ATTACHABLE = '<attachable>';
const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
/* -( Configuring Storage )------------------------------------------------ */
/**
* @task config
*/
public static function pushStorageNamespace($namespace) {
self::$namespaceStack[] = $namespace;
}
/**
* @task config
*/
public static function popStorageNamespace() {
array_pop(self::$namespaceStack);
}
/**
* @task config
*/
public static function getDefaultStorageNamespace() {
return PhabricatorEnv::getEnvConfig('storage.default-namespace');
}
/**
* @task config
*/
public static function getStorageNamespace() {
$namespace = end(self::$namespaceStack);
if (!strlen($namespace)) {
$namespace = self::getDefaultStorageNamespace();
}
if (!strlen($namespace)) {
throw new Exception(pht('No storage namespace configured!'));
}
return $namespace;
}
/**
* @task config
*/
protected function establishLiveConnection($mode) {
$namespace = self::getStorageNamespace();
$database = $namespace.'_'.$this->getApplicationName();
$is_readonly = PhabricatorEnv::isReadOnly();
if ($is_readonly && ($mode != 'r')) {
$this->raiseImproperWrite($database);
}
$connection = $this->newClusterConnection(
$this->getApplicationName(),
$database,
$mode);
// TODO: This should be testing if the mode is "r", but that would probably
// break a lot of things. Perform a more narrow test for readonly mode
// until we have greater certainty that this works correctly most of the
// time.
if ($is_readonly) {
$connection->setReadOnly(true);
}
return $connection;
}
private function newClusterConnection($application, $database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
$application);
+ $master_exception = null;
+
if ($master && !$master->isSevered()) {
$connection = $master->newApplicationConnection($database);
if ($master->isReachable($connection)) {
return $connection;
} else {
if ($mode == 'w') {
$this->raiseImpossibleWrite($database);
}
PhabricatorEnv::setReadOnly(
true,
PhabricatorEnv::READONLY_UNREACHABLE);
+
+ $master_exception = $master->getConnectionException();
}
}
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForApplication(
$application);
if ($replica) {
$connection = $replica->newApplicationConnection($database);
$connection->setReadOnly(true);
if ($replica->isReachable($connection)) {
return $connection;
}
}
if (!$master && !$replica) {
$this->raiseUnconfigured($database);
}
- $this->raiseUnreachable($database);
+ $this->raiseUnreachable($database, $master_exception);
}
private function raiseImproperWrite($database) {
throw new PhabricatorClusterImproperWriteException(
pht(
'Unable to establish a write-mode connection (to application '.
'database "%s") because Phabricator is in read-only mode. Whatever '.
'you are trying to do does not function correctly in read-only mode.',
$database));
}
private function raiseImpossibleWrite($database) {
throw new PhabricatorClusterImpossibleWriteException(
pht(
'Unable to connect to master database ("%s"). This is a severe '.
'failure; your request did not complete.',
$database));
}
private function raiseUnconfigured($database) {
throw new Exception(
pht(
'Unable to establish a connection to any database host '.
'(while trying "%s"). No masters or replicas are configured.',
$database));
}
- private function raiseUnreachable($database) {
- throw new PhabricatorClusterStrandedException(
- pht(
- 'Unable to establish a connection to any database host '.
- '(while trying "%s"). All masters and replicas are completely '.
- 'unreachable.',
- $database));
+ private function raiseUnreachable($database, Exception $proxy = null) {
+ $message = pht(
+ 'Unable to establish a connection to any database host '.
+ '(while trying "%s"). All masters and replicas are completely '.
+ 'unreachable.',
+ $database);
+
+ if ($proxy) {
+ $proxy_message = pht(
+ '%s: %s',
+ get_class($proxy),
+ $proxy->getMessage());
+ $message = $message."\n\n".$proxy_message;
+ }
+
+ throw new PhabricatorClusterStrandedException($message);
}
/**
* @task config
*/
public function getTableName() {
$str = 'phabricator';
$len = strlen($str);
$class = strtolower(get_class($this));
if (!strncmp($class, $str, $len)) {
$class = substr($class, $len);
}
$app = $this->getApplicationName();
if (!strncmp($class, $app, strlen($app))) {
$class = substr($class, strlen($app));
}
if (strlen($class)) {
return $app.'_'.$class;
} else {
return $app;
}
}
/**
* @task config
*/
abstract public function getApplicationName();
protected function getConnectionNamespace() {
return self::getStorageNamespace().'_'.$this->getApplicationName();
}
/**
* Break a list of escaped SQL statement fragments (e.g., VALUES lists for
* INSERT, previously built with @{function:qsprintf}) into chunks which will
* fit under the MySQL 'max_allowed_packet' limit.
*
* Chunks are glued together with `$glue`, by default ", ".
*
* If a statement is too large to fit within the limit, it is broken into
* its own chunk (but might fail when the query executes).
*/
public static function chunkSQL(
array $fragments,
$glue = ', ',
$limit = null) {
if ($limit === null) {
// NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer.
// Eventually we could query MySQL or let the user configure it.
$limit = (int)((1024 * 1024) * 0.90);
}
$result = array();
$chunk = array();
$len = 0;
$glue_len = strlen($glue);
foreach ($fragments as $fragment) {
$this_len = strlen($fragment);
if ($chunk) {
// Chunks after the first also imply glue.
$this_len += $glue_len;
}
if ($len + $this_len <= $limit) {
$len += $this_len;
$chunk[] = $fragment;
} else {
if ($chunk) {
$result[] = $chunk;
}
$len = strlen($fragment);
$chunk = array($fragment);
}
}
if ($chunk) {
$result[] = $chunk;
}
foreach ($result as $key => $fragment_list) {
$result[$key] = implode($glue, $fragment_list);
}
return $result;
}
protected function assertAttached($property) {
if ($property === self::ATTACHABLE) {
throw new PhabricatorDataNotAttachedException($this);
}
return $property;
}
protected function assertAttachedKey($value, $key) {
$this->assertAttached($value);
if (!array_key_exists($key, $value)) {
throw new PhabricatorDataNotAttachedException($this);
}
return $value[$key];
}
protected function detectEncodingForStorage($string) {
return phutil_is_utf8($string) ? 'utf8' : null;
}
protected function getUTF8StringFromStorage($string, $encoding) {
if ($encoding == 'utf8') {
return $string;
}
if (function_exists('mb_detect_encoding')) {
if (strlen($encoding)) {
$try_encodings = array(
$encoding,
);
} else {
// TODO: This is pretty much a guess, and probably needs to be
// configurable in the long run.
$try_encodings = array(
'JIS',
'EUC-JP',
'SJIS',
'ISO-8859-1',
);
}
$guess = mb_detect_encoding($string, $try_encodings);
if ($guess) {
return mb_convert_encoding($string, 'UTF-8', $guess);
}
}
return phutil_utf8ize($string);
}
protected function willReadData(array &$data) {
parent::willReadData($data);
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willReadValue($data[$key]);
}
}
}
protected function willWriteData(array &$data) {
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willWriteValue($data[$key]);
}
}
parent::willWriteData($data);
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php
new file mode 100644
index 000000000..03aaf5707
--- /dev/null
+++ b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php
@@ -0,0 +1,41 @@
+<?php
+
+final class PhabricatorQueryIterator extends PhutilBufferedIterator {
+
+ private $query;
+ private $pager;
+
+ public function __construct(PhabricatorCursorPagedPolicyAwareQuery $query) {
+ $this->query = $query;
+ }
+
+ protected function didRewind() {
+ $this->pager = new AphrontCursorPagerView();
+ }
+
+ public function key() {
+ return $this->current()->getID();
+ }
+
+ protected function loadPage() {
+ if (!$this->pager) {
+ return array();
+ }
+
+ $pager = clone $this->pager;
+ $query = clone $this->query;
+
+ $results = $query->executeWithCursorPager($pager);
+
+ // If we got less than a full page of results, this was the last set of
+ // results. Throw away the pager so we end iteration.
+ if (!$pager->getHasMoreResults()) {
+ $this->pager = null;
+ } else {
+ $this->pager->setAfterID($pager->getNextPageID());
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
index aacad288b..7d0946c8c 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
@@ -1,95 +1,107 @@
<?php
final class PhabricatorStorageManagementDestroyWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('destroy')
->setExamples('**destroy** [__options__]')
->setSynopsis(pht('Permanently destroy all storage and data.'))
->setArguments(
array(
array(
'name' => 'unittest-fixtures',
'help' => pht(
'Restrict **destroy** operations to databases created '.
'by %s test fixtures.',
'PhabricatorTestCase'),
),
));
}
public function didExecute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
if (!$this->isDryRun() && !$this->isForce()) {
- $console->writeOut(
- phutil_console_wrap(
- pht(
- 'Are you completely sure you really want to permanently destroy '.
- 'all storage for Phabricator data? This operation can not be '.
- 'undone and your data will not be recoverable if you proceed.')));
+ if ($args->getArg('unittest-fixtures')) {
+ $console->writeOut(
+ phutil_console_wrap(
+ pht(
+ 'Are you completely sure you really want to destroy all unit '.
+ 'test fixure data? This operation can not be undone.')));
+ if (!phutil_console_confirm(pht('Destroy all unit test data?'))) {
+ $console->writeOut("%s\n", pht('Cancelled.'));
+ exit(1);
+ }
+ } else {
+ $console->writeOut(
+ phutil_console_wrap(
+ pht(
+ 'Are you completely sure you really want to permanently destroy '.
+ 'all storage for Phabricator data? This operation can not be '.
+ 'undone and your data will not be recoverable if you proceed.')));
- if (!phutil_console_confirm(pht('Permanently destroy all data?'))) {
- $console->writeOut("%s\n", pht('Cancelled.'));
- exit(1);
- }
+ if (!phutil_console_confirm(pht('Permanently destroy all data?'))) {
+ $console->writeOut("%s\n", pht('Cancelled.'));
+ exit(1);
+ }
- if (!phutil_console_confirm(pht('Really destroy all data forever?'))) {
- $console->writeOut("%s\n", pht('Cancelled.'));
- exit(1);
+ if (!phutil_console_confirm(pht('Really destroy all data forever?'))) {
+ $console->writeOut("%s\n", pht('Cancelled.'));
+ exit(1);
+ }
}
}
$apis = $this->getMasterAPIs();
foreach ($apis as $api) {
$patches = $this->getPatches();
if ($args->getArg('unittest-fixtures')) {
$conn = $api->getConn(null);
$databases = queryfx_all(
$conn,
'SELECT DISTINCT(TABLE_SCHEMA) AS db '.
'FROM INFORMATION_SCHEMA.TABLES '.
'WHERE TABLE_SCHEMA LIKE %>',
PhabricatorTestCase::NAMESPACE_PREFIX);
$databases = ipull($databases, 'db');
} else {
$databases = $api->getDatabaseList($patches);
$databases[] = $api->getDatabaseName('meta_data');
// These are legacy databases that were dropped long ago. See T2237.
$databases[] = $api->getDatabaseName('phid');
$databases[] = $api->getDatabaseName('directory');
}
foreach ($databases as $database) {
if ($this->isDryRun()) {
$console->writeOut(
"%s\n",
pht("DRYRUN: Would drop database '%s'.", $database));
} else {
$console->writeOut(
"%s\n",
pht("Dropping database '%s'...", $database));
queryfx(
$api->getConn(null),
'DROP DATABASE IF EXISTS %T',
$database);
}
}
if (!$this->isDryRun()) {
$console->writeOut(
"%s\n",
pht(
'Storage on "%s" was destroyed.',
$api->getRef()->getRefKey()));
}
}
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
index a75afe69e..5c55dc779 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
@@ -1,106 +1,116 @@
<?php
final class PhabricatorStorageManagementProbeWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('probe')
->setExamples('**probe**')
->setSynopsis(pht('Show approximate table sizes.'));
}
protected function isReadOnlyWorkflow() {
return true;
}
public function didExecute(PhutilArgumentParser $args) {
$api = $this->getSingleAPI();
$console = PhutilConsole::getConsole();
$console->writeErr(
"%s\n",
pht('Analyzing table sizes (this may take a moment)...'));
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, true);
$conn_r = $api->getConn(null);
$data = array();
foreach ($databases as $database) {
queryfx($conn_r, 'USE %C', $database);
$tables = queryfx_all(
$conn_r,
'SHOW TABLE STATUS');
$tables = ipull($tables, null, 'Name');
$data[$database] = $tables;
}
$totals = array_fill_keys(array_keys($data), 0);
$overall = 0;
foreach ($data as $db => $tables) {
foreach ($tables as $table => $info) {
$table_size = $info['Data_length'] + $info['Index_length'];
$data[$db][$table]['_totalSize'] = $table_size;
$totals[$db] += $table_size;
$overall += $table_size;
}
}
asort($totals);
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
->setPadding(2)
->addColumn('name', array('title' => pht('Database / Table')))
- ->addColumn('size', array('title' => pht('Size')))
- ->addColumn('percentage', array('title' => pht('Percentage')));
+ ->addColumn(
+ 'size',
+ array(
+ 'title' => pht('Size'),
+ 'align' => PhutilConsoleTable::ALIGN_RIGHT,
+ ))
+ ->addColumn(
+ 'percentage',
+ array(
+ 'title' => pht('Percentage'),
+ 'align' => PhutilConsoleTable::ALIGN_RIGHT,
+ ));
foreach ($totals as $db => $size) {
list($database_size, $database_percentage) = $this->formatSize(
$totals[$db],
$overall);
$table->addRow(array(
'name' => tsprintf('**%s**', $db),
'size' => tsprintf('**%s**', $database_size),
'percentage' => tsprintf('**%s**', $database_percentage),
));
$data[$db] = isort($data[$db], '_totalSize');
foreach ($data[$db] as $table_name => $info) {
list($table_size, $table_percentage) = $this->formatSize(
$info['_totalSize'],
$overall);
$table->addRow(array(
'name' => ' '.$table_name,
'size' => $table_size,
'percentage' => $table_percentage,
));
}
}
list($overall_size, $overall_percentage) = $this->formatSize(
$overall,
$overall);
$table->addRow(array(
'name' => tsprintf('**%s**', pht('TOTAL')),
'size' => tsprintf('**%s**', $overall_size),
'percentage' => tsprintf('**%s**', $overall_percentage),
));
$table->draw();
return 0;
}
private function formatSize($n, $o) {
return array(
- sprintf('%8.8s MB', number_format($n / (1024 * 1024), 1)),
+ pht('%s MB', new PhutilNumber($n / (1024 * 1024), 1)),
sprintf('%3.1f%%', 100 * ($n / $o)),
);
}
}
diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php
index ed6cde377..e030b7d17 100644
--- a/src/view/form/control/AphrontFormCheckboxControl.php
+++ b/src/view/form/control/AphrontFormCheckboxControl.php
@@ -1,88 +1,116 @@
<?php
final class AphrontFormCheckboxControl extends AphrontFormControl {
private $boxes = array();
private $checkboxKey;
public function setCheckboxKey($checkbox_key) {
$this->checkboxKey = $checkbox_key;
return $this;
}
public function getCheckboxKey() {
return $this->checkboxKey;
}
public function addCheckbox(
$name,
$value,
$label,
$checked = false,
$id = null) {
$this->boxes[] = array(
'name' => $name,
'value' => $value,
'label' => $label,
'checked' => $checked,
'id' => $id,
);
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-checkbox';
}
+ public function setOptions(array $options) {
+ $boxes = array();
+ foreach ($options as $key => $value) {
+ $boxes[] = array(
+ 'value' => $key,
+ 'label' => $value,
+ );
+ }
+
+ $this->boxes = $boxes;
+
+ return $this;
+ }
+
protected function renderInput() {
$rows = array();
foreach ($this->boxes as $box) {
$id = idx($box, 'id');
if ($id === null) {
$id = celerity_generate_unique_node_id();
}
+
+ $name = idx($box, 'name');
+ if ($name === null) {
+ $name = $this->getName().'[]';
+ }
+
+ $value = $box['value'];
+
+ if (array_key_exists('checked', $box)) {
+ $checked = $box['checked'];
+ } else {
+ $checked = in_array($value, $this->getValue());
+ }
+
$checkbox = phutil_tag(
'input',
array(
'id' => $id,
'type' => 'checkbox',
- 'name' => $box['name'],
+ 'name' => $name,
'value' => $box['value'],
- 'checked' => $box['checked'] ? 'checked' : null,
+ 'checked' => $checked ? 'checked' : null,
'disabled' => $this->getDisabled() ? 'disabled' : null,
));
$label = phutil_tag(
'label',
array(
'for' => $id,
),
$box['label']);
$rows[] = phutil_tag('tr', array(), array(
phutil_tag('td', array(), $checkbox),
phutil_tag('th', array(), $label),
));
}
// When a user submits a form with a checkbox unchecked, the browser
// doesn't submit anything to the server. This hidden key lets the server
// know that the checkboxes were present on the client, the user just did
// not select any of them.
$checkbox_key = $this->getCheckboxKey();
if ($checkbox_key) {
$rows[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $checkbox_key,
'value' => 1,
));
}
return phutil_tag(
'table',
array('class' => 'aphront-form-control-checkbox-layout'),
$rows);
}
}
diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php
index 0b0e60804..53e6f940c 100644
--- a/src/view/form/control/AphrontFormPolicyControl.php
+++ b/src/view/form/control/AphrontFormPolicyControl.php
@@ -1,401 +1,391 @@
<?php
final class AphrontFormPolicyControl extends AphrontFormControl {
private $object;
private $capability;
private $policies;
private $spacePHID;
private $templatePHIDType;
private $templateObject;
public function setPolicyObject(PhabricatorPolicyInterface $object) {
$this->object = $object;
return $this;
}
public function setPolicies(array $policies) {
assert_instances_of($policies, 'PhabricatorPolicy');
$this->policies = $policies;
return $this;
}
public function setSpacePHID($space_phid) {
$this->spacePHID = $space_phid;
return $this;
}
public function getSpacePHID() {
return $this->spacePHID;
}
public function setTemplatePHIDType($type) {
$this->templatePHIDType = $type;
return $this;
}
public function setTemplateObject($object) {
$this->templateObject = $object;
return $this;
}
public function getSerializedValue() {
return json_encode(array(
$this->getValue(),
$this->getSpacePHID(),
));
}
public function readSerializedValue($value) {
$decoded = phutil_json_decode($value);
$policy_value = $decoded[0];
$space_phid = $decoded[1];
$this->setValue($policy_value);
$this->setSpacePHID($space_phid);
return $this;
}
public function readValueFromDictionary(array $dictionary) {
// TODO: This is a little hacky but will only get us into trouble if we
// have multiple view policy controls in multiple paged form views on the
// same page, which seems unlikely.
$this->setSpacePHID(idx($dictionary, 'spacePHID'));
return parent::readValueFromDictionary($dictionary);
}
public function readValueFromRequest(AphrontRequest $request) {
// See note in readValueFromDictionary().
$this->setSpacePHID($request->getStr('spacePHID'));
return parent::readValueFromRequest($request);
}
public function setCapability($capability) {
$this->capability = $capability;
$labels = array(
PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'),
PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'),
PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'),
);
if (isset($labels[$capability])) {
$label = $labels[$capability];
} else {
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if ($capobj) {
$label = $capobj->getCapabilityName();
} else {
$label = pht('Capability "%s"', $capability);
}
}
$this->setLabel($label);
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-policy';
}
protected function getOptions() {
$capability = $this->capability;
$policies = $this->policies;
$viewer = $this->getUser();
// Check if we're missing the policy for the current control value. This
// is unusual, but can occur if the user is submitting a form and selected
// an unusual project as a policy but the change has not been saved yet.
$policy_map = mpull($policies, null, 'getPHID');
$value = $this->getValue();
if ($value && empty($policy_map[$value])) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($value))
->executeOne();
if ($handle->isComplete()) {
$policies[] = PhabricatorPolicy::newFromPolicyAndHandle(
$value,
$handle);
}
}
// Exclude object policies which don't make sense here. This primarily
// filters object policies associated from template capabilities (like
// "Default Task View Policy" being set to "Task Author") so they aren't
// made available on non-template capabilities (like "Can Bulk Edit").
foreach ($policies as $key => $policy) {
if ($policy->getType() != PhabricatorPolicyType::TYPE_OBJECT) {
continue;
}
$rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy->getPHID());
if (!$rule) {
continue;
}
$target = nonempty($this->templateObject, $this->object);
if (!$rule->canApplyToObject($target)) {
unset($policies[$key]);
continue;
}
}
$options = array();
foreach ($policies as $policy) {
if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) {
// Never expose "Public" for capabilities which don't support it.
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
continue;
}
}
$options[$policy->getType()][$policy->getPHID()] = array(
'name' => $policy->getName(),
'full' => $policy->getName(),
'icon' => $policy->getIcon(),
'sort' => phutil_utf8_strtolower($policy->getName()),
);
}
$type_project = PhabricatorPolicyType::TYPE_PROJECT;
// Make sure we have a "Projects" group before we adjust it.
if (empty($options[$type_project])) {
$options[$type_project] = array();
}
$options[$type_project] = isort($options[$type_project], 'sort');
$placeholder = id(new PhabricatorPolicy())
->setName(pht('Other Project...'))
->setIcon('fa-search');
$options[$type_project][$this->getSelectProjectKey()] = array(
'name' => $placeholder->getName(),
'full' => $placeholder->getName(),
'icon' => $placeholder->getIcon(),
);
// If we were passed several custom policy options, throw away the ones
// which aren't the value for this capability. For example, an object might
// have a custom view policy and a custom edit policy. When we render
// the selector for "Can View", we don't want to show the "Can Edit"
// custom policy -- if we did, the menu would look like this:
//
// Custom
// Custom Policy
// Custom Policy
//
// ...where one is the "view" custom policy, and one is the "edit" custom
// policy.
$type_custom = PhabricatorPolicyType::TYPE_CUSTOM;
if (!empty($options[$type_custom])) {
$options[$type_custom] = array_select_keys(
$options[$type_custom],
array($this->getValue()));
}
// If there aren't any custom policies, add a placeholder policy so we
// render a menu item. This allows the user to switch to a custom policy.
if (empty($options[$type_custom])) {
$placeholder = new PhabricatorPolicy();
$placeholder->setName(pht('Custom Policy...'));
$options[$type_custom][$this->getSelectCustomKey()] = array(
'name' => $placeholder->getName(),
'full' => $placeholder->getName(),
'icon' => $placeholder->getIcon(),
);
}
$options = array_select_keys(
$options,
array(
PhabricatorPolicyType::TYPE_GLOBAL,
PhabricatorPolicyType::TYPE_OBJECT,
PhabricatorPolicyType::TYPE_USER,
PhabricatorPolicyType::TYPE_CUSTOM,
PhabricatorPolicyType::TYPE_PROJECT,
));
return $options;
}
protected function renderInput() {
if (!$this->object) {
throw new PhutilInvalidStateException('setPolicyObject');
}
if (!$this->capability) {
throw new PhutilInvalidStateException('setCapability');
}
$policy = $this->object->getPolicy($this->capability);
if (!$policy) {
// TODO: Make this configurable.
$policy = PhabricatorPolicies::POLICY_USER;
}
if (!$this->getValue()) {
$this->setValue($policy);
}
$control_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$caret = phutil_tag(
'span',
array(
'class' => 'caret',
));
$input = phutil_tag(
'input',
array(
'type' => 'hidden',
'id' => $input_id,
'name' => $this->getName(),
'value' => $this->getValue(),
));
$options = $this->getOptions();
$order = array();
$labels = array();
foreach ($options as $key => $values) {
$order[$key] = array_keys($values);
$labels[$key] = PhabricatorPolicyType::getPolicyTypeName($key);
}
$flat_options = array_mergev($options);
$icons = array();
foreach (igroup($flat_options, 'icon') as $icon => $ignored) {
$icons[$icon] = id(new PHUIIconView())
->setIcon($icon);
}
-
if ($this->templatePHIDType) {
$context_path = 'template/'.$this->templatePHIDType.'/';
} else {
$object_phid = $this->object->getPHID();
if ($object_phid) {
$context_path = 'object/'.$object_phid.'/';
} else {
$object_type = phid_get_type($this->object->generatePHID());
$context_path = 'type/'.$object_type.'/';
}
}
Javelin::initBehavior(
'policy-control',
array(
'controlID' => $control_id,
'inputID' => $input_id,
'options' => $flat_options,
'groups' => array_keys($options),
'order' => $order,
'labels' => $labels,
'value' => $this->getValue(),
'capability' => $this->capability,
'editURI' => '/policy/edit/'.$context_path,
'customKey' => $this->getSelectCustomKey(),
'projectKey' => $this->getSelectProjectKey(),
'disabled' => $this->getDisabled(),
));
$selected = idx($flat_options, $this->getValue(), array());
$selected_icon = idx($selected, 'icon');
$selected_name = idx($selected, 'name');
$spaces_control = $this->buildSpacesControl();
return phutil_tag(
'div',
array(
),
array(
$spaces_control,
javelin_tag(
'a',
array(
'class' => 'button button-grey dropdown has-icon has-text '.
'policy-control',
'href' => '#',
'mustcapture' => true,
'sigil' => 'policy-control',
'id' => $control_id,
),
array(
$caret,
javelin_tag(
'span',
array(
'sigil' => 'policy-label',
'class' => 'phui-button-text',
),
array(
idx($icons, $selected_icon),
$selected_name,
)),
)),
$input,
));
-
- return AphrontFormSelectControl::renderSelectTag(
- $this->getValue(),
- $this->getOptions(),
- array(
- 'name' => $this->getName(),
- 'disabled' => $this->getDisabled() ? 'disabled' : null,
- 'id' => $this->getID(),
- ));
}
public static function getSelectCustomKey() {
return 'select:custom';
}
public static function getSelectProjectKey() {
return 'select:project';
}
private function buildSpacesControl() {
if ($this->capability != PhabricatorPolicyCapability::CAN_VIEW) {
return null;
}
if (!($this->object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getUser();
if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
return null;
}
$space_phid = $this->getSpacePHID();
if ($space_phid === null) {
$space_phid = $viewer->getDefaultSpacePHID();
}
$select = AphrontFormSelectControl::renderSelectTag(
$space_phid,
PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
$viewer,
$space_phid),
array(
'disabled' => ($this->getDisabled() ? 'disabled' : null),
'name' => 'spacePHID',
'class' => 'aphront-space-select-control-knob',
));
return $select;
}
}
diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php
index 19126d1c6..ae20fe250 100644
--- a/src/view/layout/PhabricatorSourceCodeView.php
+++ b/src/view/layout/PhabricatorSourceCodeView.php
@@ -1,210 +1,253 @@
<?php
final class PhabricatorSourceCodeView extends AphrontView {
private $lines;
private $uri;
private $highlights = array();
private $canClickHighlight = true;
private $truncatedFirstBytes = false;
private $truncatedFirstLines = false;
private $symbolMetadata;
private $blameMap;
+ private $coverage = array();
public function setLines(array $lines) {
$this->lines = $lines;
return $this;
}
public function setURI(PhutilURI $uri) {
$this->uri = $uri;
return $this;
}
public function setHighlights(array $array) {
$this->highlights = array_fuse($array);
return $this;
}
public function disableHighlightOnClick() {
$this->canClickHighlight = false;
return $this;
}
public function setTruncatedFirstBytes($truncated_first_bytes) {
$this->truncatedFirstBytes = $truncated_first_bytes;
return $this;
}
public function setTruncatedFirstLines($truncated_first_lines) {
$this->truncatedFirstLines = $truncated_first_lines;
return $this;
}
public function setSymbolMetadata(array $symbol_metadata) {
$this->symbolMetadata = $symbol_metadata;
return $this;
}
public function getSymbolMetadata() {
return $this->symbolMetadata;
}
public function setBlameMap(array $map) {
$this->blameMap = $map;
return $this;
}
public function getBlameMap() {
return $this->blameMap;
}
+ public function setCoverage(array $coverage) {
+ $this->coverage = $coverage;
+ return $this;
+ }
+
+ public function getCoverage() {
+ return $this->coverage;
+ }
+
public function render() {
$blame_map = $this->getBlameMap();
$has_blame = ($blame_map !== null);
require_celerity_resource('phabricator-source-code-view-css');
require_celerity_resource('syntax-highlighting-css');
if ($this->canClickHighlight) {
Javelin::initBehavior('phabricator-line-linker');
}
$line_number = 1;
$rows = array();
$lines = $this->lines;
if ($this->truncatedFirstLines) {
$lines[] = phutil_tag(
'span',
array(
'class' => 'c',
),
pht('...'));
} else if ($this->truncatedFirstBytes) {
$last_key = last_key($lines);
$lines[$last_key] = hsprintf(
'%s%s',
$lines[$last_key],
phutil_tag(
'span',
array(
'class' => 'c',
),
pht('...')));
}
$base_uri = (string)$this->uri;
$wrote_anchor = false;
+
+ $coverage = $this->getCoverage();
+ $coverage_count = count($coverage);
+ $coverage_data = ipull($coverage, 'data');
+
+ // TODO: Modularize this properly, see T13125.
+ $coverage_map = array(
+ 'C' => 'background: #66bbff;',
+ 'U' => 'background: #dd8866;',
+ 'N' => 'background: #ddeeff;',
+ 'X' => 'background: #aa00aa;',
+ );
+
foreach ($lines as $line) {
$row_attributes = array();
if (isset($this->highlights[$line_number])) {
$row_attributes['class'] = 'phabricator-source-highlight';
if (!$wrote_anchor) {
$row_attributes['id'] = 'phabricator-line-linker-anchor';
$wrote_anchor = true;
}
}
if ($this->canClickHighlight) {
if ($base_uri) {
$line_href = $base_uri.'$'.$line_number;
} else {
$line_href = null;
}
$tag_number = phutil_tag(
'a',
array(
'href' => $line_href,
'data-n' => $line_number,
));
} else {
$tag_number = phutil_tag(
'span',
array(),
$line_number);
}
if ($has_blame) {
$lines = idx($blame_map, $line_number);
if ($lines) {
$skip_blame = 'skip';
$info_blame = 'info';
} else {
$skip_blame = null;
$info_blame = null;
}
$blame_cells = array(
phutil_tag(
'th',
array(
'class' => 'phabricator-source-blame-skip',
'data-blame' => $skip_blame,
)),
phutil_tag(
'th',
array(
'class' => 'phabricator-source-blame-info',
'data-blame' => $info_blame,
'data-blame-lines' => $lines,
)),
);
} else {
$blame_cells = null;
}
+ $coverage_cells = array();
+ foreach ($coverage as $coverage_idx => $coverage_spec) {
+ if (isset($coverage_spec['data'][$line_number - 1])) {
+ $coverage_char = $coverage_spec['data'][$line_number - 1];
+ } else {
+ $coverage_char = null;
+ }
+
+ $coverage_style = idx($coverage_map, $coverage_char, null);
+
+ $coverage_cells[] = phutil_tag(
+ 'th',
+ array(
+ 'class' => 'phabricator-source-coverage',
+ 'style' => $coverage_style,
+ 'data-coverage' => $coverage_idx.'/'.$coverage_char,
+ ));
+ }
+
$rows[] = phutil_tag(
'tr',
$row_attributes,
array(
$blame_cells,
phutil_tag(
'th',
array(
'class' => 'phabricator-source-line',
),
$tag_number),
phutil_tag(
'td',
array(
'class' => 'phabricator-source-code',
),
$line),
- ));
+ $coverage_cells,
+ ));
$line_number++;
}
$classes = array();
$classes[] = 'phabricator-source-code-view';
$classes[] = 'remarkup-code';
$classes[] = 'PhabricatorMonospaced';
$symbol_metadata = $this->getSymbolMetadata();
$sigils = array();
$sigils[] = 'phabricator-source';
$sigils[] = 'has-symbols';
Javelin::initBehavior('repository-crossreference');
return phutil_tag_div(
'phabricator-source-code-container',
javelin_tag(
'table',
array(
'class' => implode(' ', $classes),
'sigil' => implode(' ', $sigils),
'meta' => array(
'uri' => (string)$this->uri,
'symbols' => $symbol_metadata,
),
),
phutil_implode_html('', $rows)));
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php
index 0b2ca8ab1..15319a357 100644
--- a/src/view/page/menu/PhabricatorMainMenuSearchView.php
+++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php
@@ -1,234 +1,238 @@
<?php
final class PhabricatorMainMenuSearchView extends AphrontView {
const DEFAULT_APPLICATION_ICON = 'fa-dot-circle-o';
private $id;
private $application;
public function setApplication(PhabricatorApplication $application) {
$this->application = $application;
return $this;
}
public function getApplication() {
return $this->application;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function render() {
$viewer = $this->getViewer();
$target_id = celerity_generate_unique_node_id();
$search_id = $this->getID();
$button_id = celerity_generate_unique_node_id();
$selector_id = celerity_generate_unique_node_id();
$application_id = celerity_generate_unique_node_id();
$input = phutil_tag(
'input',
array(
'type' => 'text',
'name' => 'query',
'id' => $search_id,
'autocomplete' => 'off',
'autocorrect' => 'off',
'autocapitalize' => 'off',
'spellcheck' => 'false',
));
$target = javelin_tag(
'div',
array(
'id' => $target_id,
'class' => 'phabricator-main-menu-search-target',
),
'');
$search_datasource = new PhabricatorSearchDatasource();
$scope_key = PhabricatorSearchScopeSetting::SETTINGKEY;
Javelin::initBehavior(
'phabricator-search-typeahead',
array(
'id' => $target_id,
'input' => $search_id,
'button' => $button_id,
'selectorID' => $selector_id,
'applicationID' => $application_id,
'defaultApplicationIcon' => self::DEFAULT_APPLICATION_ICON,
'appScope' => PhabricatorSearchController::SCOPE_CURRENT_APPLICATION,
'src' => $search_datasource->getDatasourceURI(),
'limit' => 10,
'placeholder' => pht('Search'),
'scopeUpdateURI' => '/settings/adjust/?key='.$scope_key,
));
$primary_input = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'search:primary',
'value' => 'true',
));
$search_text = javelin_tag(
'span',
array(
'aural' => true,
),
pht('Search'));
$selector = $this->buildModeSelector($selector_id, $application_id);
$form = phabricator_form(
$viewer,
array(
'action' => '/search/',
'method' => 'POST',
),
- phutil_tag_div('phabricator-main-menu-search-container', array(
- $input,
- phutil_tag(
- 'button',
- array(
- 'id' => $button_id,
- 'class' => 'phui-icon-view phui-font-fa fa-search',
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phabricator-main-menu-search-container',
+ ),
+ array(
+ $input,
+ phutil_tag(
+ 'button',
+ array(
+ 'id' => $button_id,
+ 'class' => 'phui-icon-view phui-font-fa fa-search',
),
- array(
- $selector,
- $search_text,
- )),
- $primary_input,
- $target,
- )));
+ $search_text),
+ $selector,
+ $primary_input,
+ $target,
+ )));
return $form;
}
private function buildModeSelector($selector_id, $application_id) {
$viewer = $this->getViewer();
$items = array();
$items[] = array(
'name' => pht('Search'),
);
$items[] = array(
'icon' => 'fa-globe',
'name' => pht('All Documents'),
'value' => 'all',
);
$application_value = null;
$application_icon = self::DEFAULT_APPLICATION_ICON;
$application = $this->getApplication();
if ($application) {
$application_value = get_class($application);
if ($application->getApplicationSearchDocumentTypes()) {
$application_icon = $application->getIcon();
}
}
$items[] = array(
'icon' => $application_icon,
'name' => pht('Current Application'),
'value' => PhabricatorSearchController::SCOPE_CURRENT_APPLICATION,
);
$items[] = array(
'name' => pht('Saved Queries'),
);
$engine = id(new PhabricatorSearchApplicationSearchEngine())
->setViewer($viewer);
$engine_queries = $engine->loadEnabledNamedQueries();
$query_map = mpull($engine_queries, 'getQueryName', 'getQueryKey');
foreach ($query_map as $query_key => $query_name) {
if ($query_key == 'all') {
// Skip the builtin "All" query since it's redundant with the default
// setting.
continue;
}
$items[] = array(
'icon' => 'fa-certificate',
'name' => $query_name,
'value' => $query_key,
);
}
$items[] = array(
'name' => pht('More Options'),
);
$items[] = array(
'icon' => 'fa-search-plus',
'name' => pht('Advanced Search'),
'href' => '/search/query/advanced/',
);
$items[] = array(
'icon' => 'fa-book',
'name' => pht('User Guide: Search'),
'href' => PhabricatorEnv::getDoclink('Search User Guide'),
);
$scope_key = PhabricatorSearchScopeSetting::SETTINGKEY;
$current_value = $viewer->getUserSetting($scope_key);
$current_icon = 'fa-globe';
foreach ($items as $item) {
if (idx($item, 'value') == $current_value) {
$current_icon = $item['icon'];
break;
}
}
$selector = id(new PHUIButtonView())
->setID($selector_id)
->addClass('phabricator-main-menu-search-dropdown')
->addSigil('global-search-dropdown')
->setMetadata(
array(
'items' => $items,
'icon' => $current_icon,
'value' => $current_value,
))
->setIcon(
id(new PHUIIconView())
->addSigil('global-search-dropdown-icon')
->setIcon($current_icon))
+ ->setAuralLabel(pht('Configure Global Search'))
->setDropdown(true);
$input = javelin_tag(
'input',
array(
'type' => 'hidden',
'sigil' => 'global-search-dropdown-input',
'name' => 'search:scope',
'value' => $current_value,
));
$application_input = javelin_tag(
'input',
array(
'type' => 'hidden',
'id' => $application_id,
'sigil' => 'global-search-dropdown-app',
'name' => 'search:application',
'value' => $application_value,
));
return array($selector, $input, $application_input);
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php
index 583952b0a..67badce91 100644
--- a/src/view/page/menu/PhabricatorMainMenuView.php
+++ b/src/view/page/menu/PhabricatorMainMenuView.php
@@ -1,735 +1,736 @@
<?php
final class PhabricatorMainMenuView extends AphrontView {
private $controller;
private $applicationMenu;
public function setApplicationMenu(PHUIListView $application_menu) {
$this->applicationMenu = $application_menu;
return $this;
}
public function getApplicationMenu() {
return $this->applicationMenu;
}
public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
private static function getFavicons() {
$refs = array();
$refs['favicon'] = id(new PhabricatorFaviconRef())
->setWidth(64)
->setHeight(64);
$refs['message_favicon'] = id(new PhabricatorFaviconRef())
->setWidth(64)
->setHeight(64)
->setEmblems(
array(
'dot-pink',
null,
null,
null,
));
id(new PhabricatorFaviconRefQuery())
->withRefs($refs)
->execute();
return mpull($refs, 'getURI');
}
public function render() {
$viewer = $this->getViewer();
require_celerity_resource('phabricator-main-menu-view');
$header_id = celerity_generate_unique_node_id();
$menu_bar = array();
$alerts = array();
$search_button = '';
$app_button = '';
$aural = null;
$is_full = $this->isFullSession($viewer);
if ($is_full) {
list($menu, $dropdowns, $aural) = $this->renderNotificationMenu();
if (array_filter($menu)) {
$alerts[] = $menu;
}
$menu_bar = array_merge($menu_bar, $dropdowns);
$app_button = $this->renderApplicationMenuButton();
$search_button = $this->renderSearchMenuButton($header_id);
} else if (!$viewer->isLoggedIn()) {
$app_button = $this->renderApplicationMenuButton();
if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$search_button = $this->renderSearchMenuButton($header_id);
}
}
if ($search_button) {
$search_menu = $this->renderPhabricatorSearchMenu();
} else {
$search_menu = null;
}
if ($alerts) {
$alerts = javelin_tag(
'div',
array(
'class' => 'phabricator-main-menu-alerts',
'aural' => false,
),
$alerts);
}
if ($aural) {
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
phutil_implode_html(' ', $aural));
}
$extensions = PhabricatorMainMenuBarExtension::getAllEnabledExtensions();
foreach ($extensions as $extension) {
$extension
->setViewer($viewer)
->setIsFullSession($is_full);
$controller = $this->getController();
if ($controller) {
$extension->setController($controller);
$application = $controller->getCurrentApplication();
if ($application) {
$extension->setApplication($application);
}
}
}
if (!$is_full) {
foreach ($extensions as $key => $extension) {
if ($extension->shouldRequireFullSession()) {
unset($extensions[$key]);
}
}
}
foreach ($extensions as $key => $extension) {
if (!$extension->isExtensionEnabledForViewer($extension->getViewer())) {
unset($extensions[$key]);
}
}
$menus = array();
foreach ($extensions as $extension) {
foreach ($extension->buildMainMenus() as $menu) {
$menus[] = $menu;
}
}
// Because we display these with "float: right", reverse their order before
// rendering them into the document so that the extension order and display
// order are the same.
$menus = array_reverse($menus);
foreach ($menus as $menu) {
$menu_bar[] = $menu;
}
$classes = array();
$classes[] = 'phabricator-main-menu';
$classes[] = 'phabricator-main-menu-background';
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'id' => $header_id,
),
array(
$app_button,
$search_button,
$this->renderPhabricatorLogo(),
$alerts,
$aural,
$search_menu,
$menu_bar,
));
}
private function renderSearch() {
$viewer = $this->getViewer();
$result = null;
$keyboard_config = array(
'helpURI' => '/help/keyboardshortcut/',
);
if ($viewer->isLoggedIn()) {
$show_search = $viewer->isUserActivated();
} else {
$show_search = PhabricatorEnv::getEnvConfig('policy.allow-public');
}
if ($show_search) {
$search = new PhabricatorMainMenuSearchView();
$search->setViewer($viewer);
$application = null;
$controller = $this->getController();
if ($controller) {
$application = $controller->getCurrentApplication();
}
if ($application) {
$search->setApplication($application);
}
$result = $search;
$keyboard_config['searchID'] = $search->getID();
}
$keyboard_config['pht'] = array(
'/' => pht('Give keyboard focus to the search box.'),
'?' => pht('Show keyboard shortcut help for the current page.'),
);
Javelin::initBehavior(
'phabricator-keyboard-shortcuts',
$keyboard_config);
if ($result) {
$result = id(new PHUIListItemView())
->addClass('phabricator-main-menu-search')
->appendChild($result);
}
return $result;
}
public function renderApplicationMenuButton() {
$dropdown = $this->renderApplicationMenu();
if (!$dropdown) {
return null;
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setIcon('fa-bars')
->addClass('phabricator-core-user-menu')
->addClass('phabricator-core-user-mobile-menu')
->setNoCSS(true)
- ->setDropdownMenu($dropdown);
+ ->setDropdownMenu($dropdown)
+ ->setAuralLabel(pht('Page Menu'));
}
private function renderApplicationMenu() {
$viewer = $this->getViewer();
$view = $this->getApplicationMenu();
if ($view) {
$items = $view->getItems();
$view = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($items as $item) {
$view->addAction(
id(new PhabricatorActionView())
->setName($item->getName())
->setHref($item->getHref())
->setType($item->getType()));
}
}
return $view;
}
public function renderSearchMenuButton($header_id) {
$button_id = celerity_generate_unique_node_id();
return javelin_tag(
'a',
array(
'class' => 'phabricator-main-menu-search-button '.
'phabricator-expand-application-menu',
'sigil' => 'jx-toggle-class',
'meta' => array(
'map' => array(
$header_id => 'phabricator-search-menu-expanded',
$button_id => 'menu-icon-selected',
),
),
),
phutil_tag(
'span',
array(
'class' => 'phabricator-menu-button-icon phui-icon-view '.
'phui-font-fa fa-search',
'id' => $button_id,
),
''));
}
private function renderPhabricatorSearchMenu() {
$view = new PHUIListView();
$view->addClass('phabricator-search-menu');
$search = $this->renderSearch();
if ($search) {
$view->addMenuItem($search);
}
return $view;
}
private function renderPhabricatorLogo() {
$custom_header = PhabricatorCustomLogoConfigType::getLogoImagePHID();
$logo_style = array();
if ($custom_header) {
$cache = PhabricatorCaches::getImmutableCache();
$cache_key_logo = 'ui.custom-header.logo-phid.v3.'.$custom_header;
$logo_uri = $cache->getKey($cache_key_logo);
if (!$logo_uri) {
// NOTE: If the file policy has been changed to be restrictive, we'll
// miss here and just show the default logo. The cache will fill later
// when someone who can see the file loads the page. This might be a
// little spooky, see T11982.
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->withPHIDs(array($custom_header))
->execute();
$file = head($files);
if ($file) {
$logo_uri = $file->getViewURI();
$cache->setKey($cache_key_logo, $logo_uri);
}
}
if ($logo_uri) {
$logo_style[] = 'background-size: 40px 40px;';
$logo_style[] = 'background-position: 0 0;';
$logo_style[] = 'background-image: url('.$logo_uri.')';
}
}
$logo_node = phutil_tag(
'span',
array(
'class' => 'phabricator-main-menu-eye',
'style' => implode(' ', $logo_style),
));
$wordmark_text = PhabricatorCustomLogoConfigType::getLogoWordmark();
if (!strlen($wordmark_text)) {
$wordmark_text = pht('Phabricator');
}
$wordmark_node = phutil_tag(
'span',
array(
'class' => 'phabricator-wordmark',
),
$wordmark_text);
return phutil_tag(
'a',
array(
'class' => 'phabricator-main-menu-brand',
'href' => '/',
),
array(
javelin_tag(
'span',
array(
'aural' => true,
),
pht('Home')),
$logo_node,
$wordmark_node,
));
}
private function renderNotificationMenu() {
$viewer = $this->getViewer();
require_celerity_resource('phabricator-notification-css');
require_celerity_resource('phabricator-notification-menu-css');
$container_classes = array('alert-notifications');
$aural = array();
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($viewer);
$dropdown_data = $dropdown_query->execute();
$message_tag = '';
$message_notification_dropdown = '';
$conpherence_app = 'PhabricatorConpherenceApplication';
$conpherence_data = $dropdown_data[$conpherence_app];
if ($conpherence_data['isInstalled']) {
$message_id = celerity_generate_unique_node_id();
$message_count_id = celerity_generate_unique_node_id();
$message_dropdown_id = celerity_generate_unique_node_id();
$message_count_number = $conpherence_data['rawCount'];
if ($message_count_number) {
$aural[] = phutil_tag(
'a',
array(
'href' => '/conpherence/',
),
pht(
'%s unread messages.',
new PhutilNumber($message_count_number)));
} else {
$aural[] = pht('No messages.');
}
$message_count_tag = phutil_tag(
'span',
array(
'id' => $message_count_id,
'class' => 'phabricator-main-menu-message-count',
),
$conpherence_data['count']);
$message_icon_tag = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-message-icon phui-icon-view '.
'phui-font-fa fa-comments',
'sigil' => 'menu-icon',
),
'');
if ($message_count_number) {
$container_classes[] = 'message-unread';
}
$message_tag = phutil_tag(
'a',
array(
'href' => '/conpherence/',
'class' => implode(' ', $container_classes),
'id' => $message_id,
),
array(
$message_icon_tag,
$message_count_tag,
));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $message_id,
'countID' => $message_count_id,
'dropdownID' => $message_dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/conpherence/panel/',
'countType' => $conpherence_data['countType'],
'countNumber' => $message_count_number,
'unreadClass' => 'message-unread',
) + self::getFavicons());
$message_notification_dropdown = javelin_tag(
'div',
array(
'id' => $message_dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
}
$bubble_tag = '';
$notification_dropdown = '';
$notification_app = 'PhabricatorNotificationsApplication';
$notification_data = $dropdown_data[$notification_app];
if ($notification_data['isInstalled']) {
$count_id = celerity_generate_unique_node_id();
$dropdown_id = celerity_generate_unique_node_id();
$bubble_id = celerity_generate_unique_node_id();
$count_number = $notification_data['rawCount'];
if ($count_number) {
$aural[] = phutil_tag(
'a',
array(
'href' => '/notification/',
),
pht(
'%s unread notifications.',
new PhutilNumber($count_number)));
} else {
$aural[] = pht('No notifications.');
}
$count_tag = phutil_tag(
'span',
array(
'id' => $count_id,
'class' => 'phabricator-main-menu-alert-count',
),
$notification_data['count']);
$icon_tag = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-alert-icon phui-icon-view '.
'phui-font-fa fa-bell',
'sigil' => 'menu-icon',
),
'');
if ($count_number) {
$container_classes[] = 'alert-unread';
}
$bubble_tag = phutil_tag(
'a',
array(
'href' => '/notification/',
'class' => implode(' ', $container_classes),
'id' => $bubble_id,
),
array($icon_tag, $count_tag));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $bubble_id,
'countID' => $count_id,
'dropdownID' => $dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/notification/panel/',
'countType' => $notification_data['countType'],
'countNumber' => $count_number,
'unreadClass' => 'alert-unread',
) + self::getFavicons());
$notification_dropdown = javelin_tag(
'div',
array(
'id' => $dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
}
// Admin Level Urgent Notification Channel
$setup_tag = '';
$setup_notification_dropdown = '';
if ($viewer && $viewer->getIsAdmin()) {
$open = PhabricatorSetupCheck::getOpenSetupIssueKeys();
if ($open) {
$setup_id = celerity_generate_unique_node_id();
$setup_count_id = celerity_generate_unique_node_id();
$setup_dropdown_id = celerity_generate_unique_node_id();
$setup_count_number = count($open);
if ($setup_count_number) {
$aural[] = phutil_tag(
'a',
array(
'href' => '/config/issue/',
),
pht(
'%s unresolved issues.',
new PhutilNumber($setup_count_number)));
} else {
$aural[] = pht('No issues.');
}
$setup_count_tag = phutil_tag(
'span',
array(
'id' => $setup_count_id,
'class' => 'phabricator-main-menu-setup-count',
),
$setup_count_number);
$setup_icon_tag = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-setup-icon phui-icon-view '.
'phui-font-fa fa-exclamation-circle',
'sigil' => 'menu-icon',
),
'');
if ($setup_count_number) {
$container_classes[] = 'setup-unread';
}
$setup_tag = phutil_tag(
'a',
array(
'href' => '/config/issue/',
'class' => implode(' ', $container_classes),
'id' => $setup_id,
),
array(
$setup_icon_tag,
$setup_count_tag,
));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $setup_id,
'countID' => $setup_count_id,
'dropdownID' => $setup_dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/config/issue/panel/',
'countType' => null,
'countNumber' => null,
'unreadClass' => 'setup-unread',
) + self::getFavicons());
$setup_notification_dropdown = javelin_tag(
'div',
array(
'id' => $setup_dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
}
}
$user_dropdown = null;
$user_tag = null;
if ($viewer->isLoggedIn()) {
if (!$viewer->getIsEmailVerified()) {
$bubble_id = celerity_generate_unique_node_id();
$count_id = celerity_generate_unique_node_id();
$dropdown_id = celerity_generate_unique_node_id();
$settings_uri = id(new PhabricatorEmailAddressesSettingsPanel())
->setViewer($viewer)
->setUser($viewer)
->getPanelURI();
$user_icon = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-setup-icon phui-icon-view '.
'phui-font-fa fa-user',
'sigil' => 'menu-icon',
));
$user_count = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-setup-count',
'id' => $count_id,
),
1);
$user_tag = phutil_tag(
'a',
array(
'href' => $settings_uri,
'class' => 'setup-unread',
'id' => $bubble_id,
),
array(
$user_icon,
$user_count,
));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $bubble_id,
'countID' => $count_id,
'dropdownID' => $dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/settings/issue/',
'unreadClass' => 'setup-unread',
));
$user_dropdown = javelin_tag(
'div',
array(
'id' => $dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
));
}
}
$dropdowns = array(
$notification_dropdown,
$message_notification_dropdown,
$setup_notification_dropdown,
$user_dropdown,
);
return array(
array(
$bubble_tag,
$message_tag,
$setup_tag,
$user_tag,
),
$dropdowns,
$aural,
);
}
private function isFullSession(PhabricatorUser $viewer) {
if (!$viewer->isLoggedIn()) {
return false;
}
if (!$viewer->isUserActivated()) {
return false;
}
if (!$viewer->hasSession()) {
return false;
}
$session = $viewer->getSession();
if ($session->getIsPartial()) {
return false;
}
if (!$session->getSignedLegalpadDocuments()) {
return false;
}
$mfa_key = 'security.require-multi-factor-auth';
$need_mfa = PhabricatorEnv::getEnvConfig($mfa_key);
if ($need_mfa) {
$have_mfa = $viewer->getIsEnrolledInMultiFactor();
if (!$have_mfa) {
return false;
}
}
return true;
}
}
diff --git a/src/view/phui/PHUIButtonView.php b/src/view/phui/PHUIButtonView.php
index 91c336eaa..bad24122b 100644
--- a/src/view/phui/PHUIButtonView.php
+++ b/src/view/phui/PHUIButtonView.php
@@ -1,274 +1,303 @@
<?php
final class PHUIButtonView extends AphrontTagView {
const GREEN = 'green';
const GREY = 'grey';
const BLUE = 'blue';
const RED = 'red';
const DISABLED = 'disabled';
const SMALL = 'small';
const BIG = 'big';
const BUTTONTYPE_DEFAULT = 'buttontype.default';
const BUTTONTYPE_SIMPLE = 'buttontype.simple';
private $size;
private $text;
private $subtext;
private $color;
private $tag = 'button';
private $dropdown;
private $icon;
private $iconFirst;
private $href = null;
private $title = null;
private $disabled;
private $selected;
private $name;
private $tooltip;
private $noCSS;
private $hasCaret;
private $buttonType = self::BUTTONTYPE_DEFAULT;
+ private $auralLabel;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setText($text) {
$this->text = $text;
return $this;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setSubtext($subtext) {
$this->subtext = $subtext;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function setSelected($selected) {
$this->selected = $selected;
return $this;
}
public function setTag($tag) {
$this->tag = $tag;
return $this;
}
public function setSize($size) {
$this->size = $size;
return $this;
}
public function setDropdown($dd) {
$this->dropdown = $dd;
return $this;
}
public function setTooltip($text) {
$this->tooltip = $text;
return $this;
}
public function setNoCSS($no_css) {
$this->noCSS = $no_css;
return $this;
}
public function setHasCaret($has_caret) {
$this->hasCaret = $has_caret;
return $this;
}
public function getHasCaret() {
return $this->hasCaret;
}
public function setButtonType($button_type) {
$this->buttonType = $button_type;
return $this;
}
public function getButtonType() {
return $this->buttonType;
}
+ public function setAuralLabel($aural_label) {
+ $this->auralLabel = $aural_label;
+ return $this;
+ }
+
+ public function getAuralLabel() {
+ return $this->auralLabel;
+ }
+
public function setIcon($icon, $first = true) {
if (!($icon instanceof PHUIIconView)) {
$icon = id(new PHUIIconView())
->setIcon($icon);
}
$this->icon = $icon;
$this->iconFirst = $first;
return $this;
}
protected function getTagName() {
return $this->tag;
}
public function setDropdownMenu(PhabricatorActionListView $actions) {
Javelin::initBehavior('phui-dropdown-menu');
$this->addSigil('phui-dropdown-menu');
$this->setDropdown(true);
$this->setMetadata($actions->getDropdownMenuMetadata());
return $this;
}
public function setDropdownMenuID($id) {
Javelin::initBehavior('phui-dropdown-menu');
$this->addSigil('phui-dropdown-menu');
$this->setMetadata(
array(
'menuID' => $id,
));
return $this;
}
protected function getTagAttributes() {
require_celerity_resource('phui-button-css');
require_celerity_resource('phui-button-simple-css');
$classes = array();
$classes[] = 'button';
if ($this->color) {
$classes[] = 'button-'.$this->color;
}
if ($this->size) {
$classes[] = $this->size;
}
if ($this->dropdown) {
$classes[] = 'dropdown';
}
if ($this->icon) {
$classes[] = 'has-icon';
}
if ($this->text !== null) {
$classes[] = 'has-text';
}
if ($this->iconFirst == false) {
$classes[] = 'icon-last';
}
if ($this->disabled) {
$classes[] = 'disabled';
}
if ($this->selected) {
$classes[] = 'selected';
}
switch ($this->getButtonType()) {
case self::BUTTONTYPE_DEFAULT:
$classes[] = 'phui-button-default';
break;
case self::BUTTONTYPE_SIMPLE:
$classes[] = 'phui-button-simple';
break;
}
$sigil = null;
$meta = null;
if ($this->tooltip) {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
$sigil = 'has-tooltip';
$meta = array(
'tip' => $this->tooltip,
);
}
if ($this->noCSS) {
$classes = array();
}
+ // See PHI823. If we aren't rendering a "<button>" tag, give the tag we
+ // are rendering a "button" role as a hint to screen readers.
+ $role = null;
+ if ($this->tag !== 'button') {
+ $role = 'button';
+ }
+
return array(
'class' => $classes,
'href' => $this->href,
'name' => $this->name,
'title' => $this->title,
'sigil' => $sigil,
'meta' => $meta,
+ 'role' => $role,
);
}
protected function getTagContent() {
$icon = $this->icon;
$text = null;
$subtext = null;
if ($this->subtext) {
$subtext = phutil_tag(
'div',
array(
'class' => 'phui-button-subtext',
),
$this->subtext);
}
if ($this->text !== null) {
$text = phutil_tag(
'div',
array(
'class' => 'phui-button-text',
),
array(
$this->text,
$subtext,
));
}
$caret = null;
if ($this->dropdown || $this->getHasCaret()) {
$caret = phutil_tag('span', array('class' => 'caret'), '');
}
+ $aural = null;
+ if ($this->auralLabel !== null) {
+ $aural = phutil_tag(
+ 'span',
+ array(
+ 'class' => 'aural-only',
+ ),
+ $this->auralLabel);
+ }
+
+
if ($this->iconFirst == true) {
- return array($icon, $text, $caret);
+ return array($aural, $icon, $text, $caret);
} else {
- return array($text, $icon, $caret);
+ return array($aural, $text, $icon, $caret);
}
}
}
diff --git a/src/view/phui/PHUICrumbView.php b/src/view/phui/PHUICrumbView.php
index 68c43be4e..1e5dad2ec 100644
--- a/src/view/phui/PHUICrumbView.php
+++ b/src/view/phui/PHUICrumbView.php
@@ -1,112 +1,133 @@
<?php
final class PHUICrumbView extends AphrontView {
private $name;
private $href;
private $icon;
private $isLastCrumb;
private $workflow;
private $aural;
+ private $alwaysVisible;
public function setAural($aural) {
$this->aural = $aural;
return $this;
}
public function getAural() {
return $this->aural;
}
+ /**
+ * Make this crumb always visible, even on devices where it would normally
+ * be hidden.
+ *
+ * @param bool True to make the crumb always visible.
+ * @return this
+ */
+ public function setAlwaysVisible($always_visible) {
+ $this->alwaysVisible = $always_visible;
+ return $this;
+ }
+
+ public function getAlwaysVisible() {
+ return $this->alwaysVisible;
+ }
+
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
protected function canAppendChild() {
return false;
}
public function setIsLastCrumb($is_last_crumb) {
$this->isLastCrumb = $is_last_crumb;
return $this;
}
public function render() {
$classes = array(
'phui-crumb-view',
);
$aural = null;
if ($this->aural !== null) {
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
$this->aural);
}
$icon = null;
if ($this->icon) {
$classes[] = 'phui-crumb-has-icon';
$icon = id(new PHUIIconView())
->setIcon($this->icon);
}
// Surround the crumb name with spaces so that double clicking it only
// selects the crumb itself.
$name = array(' ', $this->name);
$name = phutil_tag(
'span',
array(
'class' => 'phui-crumb-name',
),
$name);
// Because of text-overflow and safari, put the second space on the
// outside of the element.
$name = array($name, ' ');
$divider = null;
if (!$this->isLastCrumb) {
$divider = id(new PHUIIconView())
->setIcon('fa-angle-right')
->addClass('phui-crumb-divider')
->addClass('phui-crumb-view');
} else {
$classes[] = 'phabricator-last-crumb';
}
+ if ($this->getAlwaysVisible()) {
+ $classes[] = 'phui-crumb-always-visible';
+ }
+
$tag = javelin_tag(
$this->href ? 'a' : 'span',
array(
'sigil' => $this->workflow ? 'workflow' : null,
'href' => $this->href,
'class' => implode(' ', $classes),
),
array($aural, $icon, $name));
return array($tag, $divider);
}
}
diff --git a/src/view/phui/PHUIDocumentViewPro.php b/src/view/phui/PHUIDocumentView.php
similarity index 60%
rename from src/view/phui/PHUIDocumentViewPro.php
rename to src/view/phui/PHUIDocumentView.php
index 51ecc3526..f57152833 100644
--- a/src/view/phui/PHUIDocumentViewPro.php
+++ b/src/view/phui/PHUIDocumentView.php
@@ -1,145 +1,206 @@
<?php
-final class PHUIDocumentViewPro extends AphrontTagView {
+final class PHUIDocumentView extends AphrontTagView {
private $header;
private $bookname;
private $bookdescription;
private $fluid;
private $toc;
private $foot;
+ private $curtain;
+ private $banner;
public function setHeader(PHUIHeaderView $header) {
$header->setTall(true);
$this->header = $header;
return $this;
}
public function setBook($name, $description) {
$this->bookname = $name;
$this->bookdescription = $description;
return $this;
}
public function setFluid($fluid) {
$this->fluid = $fluid;
return $this;
}
public function setToc($toc) {
$this->toc = $toc;
return $this;
}
public function setFoot($foot) {
$this->foot = $foot;
return $this;
}
+ public function setCurtain(PHUICurtainView $curtain) {
+ $this->curtain = $curtain;
+ return $this;
+ }
+
+ public function getCurtain() {
+ return $this->curtain;
+ }
+
+ public function setBanner($banner) {
+ $this->banner = $banner;
+ return $this;
+ }
+
+ public function getBanner() {
+ return $this->banner;
+ }
+
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phui-document-container';
if ($this->fluid) {
$classes[] = 'phui-document-fluid';
}
if ($this->foot) {
$classes[] = 'document-has-foot';
}
return array(
'class' => implode(' ', $classes),
);
}
protected function getTagContent() {
require_celerity_resource('phui-document-view-css');
require_celerity_resource('phui-document-view-pro-css');
Javelin::initBehavior('phabricator-reveal-content');
$classes = array();
$classes[] = 'phui-document-view';
$classes[] = 'phui-document-view-pro';
+ if ($this->curtain) {
+ $classes[] = 'has-curtain';
+ } else {
+ $classes[] = 'has-no-curtain';
+ }
+
+ if ($this->curtain) {
+ $action_list = $this->curtain->getActionList();
+ $this->header->setActionListID($action_list->getID());
+ }
+
$book = null;
if ($this->bookname) {
$book = pht('%s (%s)', $this->bookname, $this->bookdescription);
}
$main_content = $this->renderChildren();
if ($book) {
$this->header->setSubheader($book);
}
$table_of_contents = null;
if ($this->toc) {
$toc = array();
$toc_id = celerity_generate_unique_node_id();
$toc[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-align-left')
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
->addClass('phui-document-toc')
->addSigil('jx-toggle-class')
->setMetaData(array(
'map' => array(
$toc_id => 'phui-document-toc-open',
),
));
$toc[] = phutil_tag(
'div',
array(
'class' => 'phui-list-sidenav phui-document-toc-list',
),
$this->toc);
$table_of_contents = phutil_tag(
'div',
array(
'class' => 'phui-document-toc-container',
'id' => $toc_id,
),
$toc);
}
$foot_content = null;
if ($this->foot) {
$foot_content = phutil_tag(
'div',
array(
'class' => 'phui-document-foot-content',
),
$this->foot);
}
- $content_inner = phutil_tag(
+ $curtain = null;
+ if ($this->curtain) {
+ $curtain = phutil_tag(
'div',
array(
- 'class' => 'phui-document-inner',
+ 'class' => 'phui-document-curtain',
),
- array(
- $table_of_contents,
- $this->header,
- $main_content,
- $foot_content,
- ));
+ $this->curtain);
+ }
- $content = phutil_tag(
- 'div',
- array(
- 'class' => 'phui-document-content',
- ),
- $content_inner);
+ $main_content = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-document-content-view',
+ ),
+ $main_content);
- return phutil_tag(
- 'div',
- array(
- 'class' => implode(' ', $classes),
- ),
- $content);
+ $content_inner = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-document-inner',
+ ),
+ array(
+ $table_of_contents,
+ $this->header,
+ $this->banner,
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-document-content-outer',
+ ),
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-document-content-inner',
+ ),
+ array(
+ $main_content,
+ $curtain,
+ ))),
+ $foot_content,
+ ));
+ $content = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-document-content',
+ ),
+ $content_inner);
+
+ return phutil_tag(
+ 'div',
+ array(
+ 'class' => implode(' ', $classes),
+ ),
+ $content);
}
}
diff --git a/src/view/phui/PHUIHeadThingView.php b/src/view/phui/PHUIHeadThingView.php
index dd788399d..ab2feee98 100644
--- a/src/view/phui/PHUIHeadThingView.php
+++ b/src/view/phui/PHUIHeadThingView.php
@@ -1,71 +1,71 @@
<?php
final class PHUIHeadThingView extends AphrontTagView {
private $image;
private $imageHref;
private $content;
private $size;
const SMALL = 'head-thing-small';
const MEDIUM = 'head-thing-medium';
public function setImageHref($href) {
$this->imageHref = $href;
return $this;
}
public function setImage($image) {
$this->image = $image;
return $this;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function setSize($size) {
$this->size = $size;
return $this;
}
protected function getTagAttributes() {
require_celerity_resource('phui-head-thing-view-css');
$classes = array();
$classes[] = 'phui-head-thing-view';
if ($this->image) {
$classes[] = 'phui-head-has-image';
}
if ($this->size) {
$classes[] = $this->size;
} else {
$classes[] = self::SMALL;
}
return array(
'class' => $classes,
);
}
protected function getTagContent() {
$image = phutil_tag(
'a',
array(
- 'class' => 'phui-head-thing-image',
+ 'class' => 'phui-head-thing-image visual-only',
'style' => 'background-image: url('.$this->image.');',
'href' => $this->imageHref,
));
if ($this->image) {
return array($image, $this->content);
} else {
return $this->content;
}
}
}
diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php
index 4ac4b2de5..54f4fa58e 100644
--- a/src/view/phui/PHUIHeaderView.php
+++ b/src/view/phui/PHUIHeaderView.php
@@ -1,554 +1,571 @@
<?php
final class PHUIHeaderView extends AphrontTagView {
const PROPERTY_STATUS = 1;
private $header;
private $tags = array();
private $image;
private $imageURL = null;
private $imageEditURL = null;
private $subheader;
private $headerIcon;
private $noBackground;
private $bleedHeader;
private $profileHeader;
private $tall;
private $properties = array();
private $actionLinks = array();
private $buttonBar = null;
private $policyObject;
private $epoch;
private $actionItems = array();
private $href;
private $actionList;
private $actionListID;
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setNoBackground($nada) {
$this->noBackground = $nada;
return $this;
}
public function setTall($tall) {
$this->tall = $tall;
return $this;
}
public function addTag(PHUITagView $tag) {
$this->tags[] = $tag;
return $this;
}
public function setImage($uri) {
$this->image = $uri;
return $this;
}
public function setImageURL($url) {
$this->imageURL = $url;
return $this;
}
public function setImageEditURL($url) {
$this->imageEditURL = $url;
return $this;
}
public function setSubheader($subheader) {
$this->subheader = $subheader;
return $this;
}
public function setBleedHeader($bleed) {
$this->bleedHeader = $bleed;
return $this;
}
public function setProfileHeader($bighead) {
$this->profileHeader = $bighead;
return $this;
}
public function setHeaderIcon($icon) {
$this->headerIcon = $icon;
return $this;
}
public function setActionList(PhabricatorActionListView $list) {
$this->actionList = $list;
return $this;
}
public function setActionListID($action_list_id) {
$this->actionListID = $action_list_id;
return $this;
}
public function setPolicyObject(PhabricatorPolicyInterface $object) {
$this->policyObject = $object;
return $this;
}
public function addProperty($property, $value) {
$this->properties[$property] = $value;
return $this;
}
public function addActionLink(PHUIButtonView $button) {
$this->actionLinks[] = $button;
return $this;
}
public function addActionItem($action) {
$this->actionItems[] = $action;
return $this;
}
public function setButtonBar(PHUIButtonBarView $bb) {
$this->buttonBar = $bb;
return $this;
}
public function setStatus($icon, $color, $name) {
// TODO: Normalize "closed/archived" to constants.
if ($color == 'dark') {
$color = PHUITagView::COLOR_INDIGO;
}
$tag = id(new PHUITagView())
->setName($name)
->setIcon($icon)
->setColor($color)
->setType(PHUITagView::TYPE_SHADE);
return $this->addProperty(self::PROPERTY_STATUS, $tag);
}
public function setEpoch($epoch) {
$age = time() - $epoch;
$age = floor($age / (60 * 60 * 24));
if ($age < 1) {
$when = pht('Today');
} else if ($age == 1) {
$when = pht('Yesterday');
} else {
$when = pht('%s Day(s) Ago', new PhutilNumber($age));
}
$this->setStatus('fa-clock-o bluegrey', null, pht('Updated %s', $when));
return $this;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
protected function getTagName() {
return 'div';
}
protected function getTagAttributes() {
require_celerity_resource('phui-header-view-css');
$classes = array();
$classes[] = 'phui-header-shell';
if ($this->noBackground) {
$classes[] = 'phui-header-no-background';
}
if ($this->bleedHeader) {
$classes[] = 'phui-bleed-header';
}
if ($this->profileHeader) {
$classes[] = 'phui-profile-header';
}
if ($this->properties || $this->policyObject ||
$this->subheader || $this->tall) {
$classes[] = 'phui-header-tall';
}
return array(
'class' => $classes,
);
}
protected function getTagContent() {
if ($this->actionList || $this->actionListID) {
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Actions'))
->setHref('#')
->setIcon('fa-bars')
->addClass('phui-mobile-menu');
if ($this->actionList) {
$action_button->setDropdownMenu($this->actionList);
} else if ($this->actionListID) {
$action_button->setDropdownMenuID($this->actionListID);
}
$this->addActionLink($action_button);
}
$image = null;
if ($this->image) {
$image_href = null;
if ($this->imageURL) {
$image_href = $this->imageURL;
} else if ($this->imageEditURL) {
$image_href = $this->imageEditURL;
}
$image = phutil_tag(
'span',
array(
'class' => 'phui-header-image',
'style' => 'background-image: url('.$this->image.')',
));
if ($image_href) {
$edit_view = null;
if ($this->imageEditURL) {
$edit_view = phutil_tag(
'span',
array(
'class' => 'phui-header-image-edit',
),
pht('Edit'));
}
$image = phutil_tag(
'a',
array(
'href' => $image_href,
'class' => 'phui-header-image-href',
),
array(
$image,
$edit_view,
));
}
}
$viewer = $this->getUser();
$left = array();
$right = array();
$space_header = null;
if ($viewer) {
$space_header = id(new PHUISpacesNamespaceContextView())
->setUser($viewer)
->setObject($this->policyObject);
}
if ($this->actionLinks) {
$actions = array();
foreach ($this->actionLinks as $button) {
if (!$button->getColor()) {
$button->setColor(PHUIButtonView::GREY);
}
$button->addClass(PHUI::MARGIN_SMALL_LEFT);
$button->addClass('phui-header-action-link');
$actions[] = $button;
}
$right[] = phutil_tag(
'div',
array(
'class' => 'phui-header-action-links',
),
$actions);
}
if ($this->buttonBar) {
$right[] = phutil_tag(
'div',
array(
'class' => 'phui-header-action-links',
),
$this->buttonBar);
}
if ($this->actionItems) {
$action_list = array();
if ($this->actionItems) {
foreach ($this->actionItems as $item) {
$action_list[] = phutil_tag(
'li',
array(
'class' => 'phui-header-action-item',
),
$item);
}
}
$right[] = phutil_tag(
'ul',
array(
'class' => 'phui-header-action-list',
),
$action_list);
}
$icon = null;
if ($this->headerIcon) {
if ($this->headerIcon instanceof PHUIIconView) {
$icon = id(clone $this->headerIcon)
->addClass('phui-header-icon');
} else {
$icon = id(new PHUIIconView())
->setIcon($this->headerIcon)
->addClass('phui-header-icon');
}
}
$header_content = $this->header;
$href = $this->getHref();
if ($href !== null) {
$header_content = phutil_tag(
'a',
array(
'href' => $href,
),
$header_content);
}
$left[] = phutil_tag(
'span',
array(
'class' => 'phui-header-header',
),
array(
$space_header,
$icon,
$header_content,
));
if ($this->subheader) {
$left[] = phutil_tag(
'div',
array(
'class' => 'phui-header-subheader',
),
array(
$this->subheader,
));
}
if ($this->properties || $this->policyObject || $this->tags) {
$property_list = array();
foreach ($this->properties as $type => $property) {
switch ($type) {
case self::PROPERTY_STATUS:
$property_list[] = $property;
break;
default:
throw new Exception(pht('Incorrect Property Passed'));
break;
}
}
if ($this->policyObject) {
$property_list[] = $this->renderPolicyProperty($this->policyObject);
}
if ($this->tags) {
$property_list[] = $this->tags;
}
$left[] = phutil_tag(
'div',
array(
'class' => 'phui-header-subheader',
),
$property_list);
}
// We here at @phabricator
$header_image = null;
if ($image) {
$header_image = phutil_tag(
'div',
array(
'class' => 'phui-header-col1',
),
$image);
}
// All really love
$header_left = phutil_tag(
'div',
array(
'class' => 'phui-header-col2',
),
$left);
// Tables and Pokemon.
$header_right = phutil_tag(
'div',
array(
'class' => 'phui-header-col3',
),
$right);
$header_row = phutil_tag(
'div',
array(
'class' => 'phui-header-row',
),
array(
$header_image,
$header_left,
$header_right,
));
return phutil_tag(
'h1',
array(
'class' => 'phui-header-view',
),
$header_row);
}
private function renderPolicyProperty(PhabricatorPolicyInterface $object) {
$viewer = $this->getUser();
$policies = PhabricatorPolicyQuery::loadPolicies($viewer, $object);
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
$policy = idx($policies, $view_capability);
if (!$policy) {
return null;
}
// If an object is in a Space with a strictly stronger (more restrictive)
// policy, we show the more restrictive policy. This better aligns the
// UI hint with the actual behavior.
// NOTE: We'll do this even if the viewer has access to only one space, and
// show them information about the existence of spaces if they click
// through.
$use_space_policy = false;
if ($object instanceof PhabricatorSpacesInterface) {
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$object);
$spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer);
$space = idx($spaces, $space_phid);
if ($space) {
$space_policies = PhabricatorPolicyQuery::loadPolicies(
$viewer,
$space);
$space_policy = idx($space_policies, $view_capability);
if ($space_policy) {
if ($space_policy->isStrongerThan($policy)) {
$policy = $space_policy;
$use_space_policy = true;
}
}
}
}
$container_classes = array();
$container_classes[] = 'policy-header-callout';
$phid = $object->getPHID();
// If we're going to show the object policy, try to determine if the object
// policy differs from the default policy. If it does, we'll call it out
// as changed.
if (!$use_space_policy) {
- $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject(
- $viewer,
- $object,
- $view_capability);
- if ($default_policy) {
- if ($default_policy->getPHID() != $policy->getPHID()) {
- $container_classes[] = 'policy-adjusted';
- if ($default_policy->isStrongerThan($policy)) {
- // The policy has strictly been weakened. For example, the
- // default might be "All Users" and the current policy is "Public".
- $container_classes[] = 'policy-adjusted-weaker';
- } else if ($policy->isStrongerThan($default_policy)) {
- // The policy has strictly been strengthened, and is now more
- // restrictive than the default. For example, "All Users" has
- // been replaced with "No One".
- $container_classes[] = 'policy-adjusted-stronger';
- } else {
- // The policy has been adjusted but not strictly strengthened
- // or weakened. For example, "Members of X" has been replaced with
- // "Members of Y".
- $container_classes[] = 'policy-adjusted-different';
+ $strength = null;
+ if ($object instanceof PhabricatorPolicyCodexInterface) {
+ $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer))
+ ->setCapability($view_capability);
+ $strength = $codex->compareToDefaultPolicy($policy);
+ } else {
+ $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject(
+ $viewer,
+ $object,
+ $view_capability);
+
+ if ($default_policy) {
+ if ($default_policy->getPHID() != $policy->getPHID()) {
+ if ($default_policy->isStrongerThan($policy)) {
+ $strength = PhabricatorPolicyStrengthConstants::WEAKER;
+ } else if ($policy->isStrongerThan($default_policy)) {
+ $strength = PhabricatorPolicyStrengthConstants::STRONGER;
+ } else {
+ $strength = PhabricatorPolicyStrengthConstants::ADJUSTED;
+ }
}
}
}
+
+ if ($strength) {
+ if ($strength == PhabricatorPolicyStrengthConstants::WEAKER) {
+ // The policy has strictly been weakened. For example, the
+ // default might be "All Users" and the current policy is "Public".
+ $container_classes[] = 'policy-adjusted-weaker';
+ } else if ($strength == PhabricatorPolicyStrengthConstants::STRONGER) {
+ // The policy has strictly been strengthened, and is now more
+ // restrictive than the default. For example, "All Users" has
+ // been replaced with "No One".
+ $container_classes[] = 'policy-adjusted-stronger';
+ } else {
+ // The policy has been adjusted but not strictly strengthened
+ // or weakened. For example, "Members of X" has been replaced with
+ // "Members of Y".
+ $container_classes[] = 'policy-adjusted-different';
+ }
+ }
}
$policy_name = array($policy->getShortName());
$policy_icon = $policy->getIcon().' bluegrey';
if ($object instanceof PhabricatorPolicyCodexInterface) {
$codex = PhabricatorPolicyCodex::newFromObject($object, $viewer);
$codex_name = $codex->getPolicyShortName($policy, $view_capability);
if ($codex_name !== null) {
$policy_name = $codex_name;
}
$codex_icon = $codex->getPolicyIcon($policy, $view_capability);
if ($codex_icon !== null) {
$policy_icon = $codex_icon;
}
$codex_classes = $codex->getPolicyTagClasses($policy, $view_capability);
foreach ($codex_classes as $codex_class) {
$container_classes[] = $codex_class;
}
}
if (!is_array($policy_name)) {
$policy_name = (array)$policy_name;
}
$arrow = id(new PHUIIconView())
->setIcon('fa-angle-right')
->addClass('policy-tier-separator');
$policy_name = phutil_implode_html($arrow, $policy_name);
$icon = id(new PHUIIconView())
->setIcon($policy_icon);
$link = javelin_tag(
'a',
array(
'class' => 'policy-link',
'href' => '/policy/explain/'.$phid.'/'.$view_capability.'/',
'sigil' => 'workflow',
),
$policy_name);
return phutil_tag(
'span',
array(
'class' => implode(' ', $container_classes),
),
array($icon, $link));
}
}
diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php
index c1e57632c..0657086c4 100644
--- a/src/view/phui/PHUIObjectItemView.php
+++ b/src/view/phui/PHUIObjectItemView.php
@@ -1,838 +1,840 @@
<?php
final class PHUIObjectItemView extends AphrontTagView {
private $objectName;
private $header;
private $subhead;
private $href;
private $attributes = array();
private $icons = array();
private $barColor;
private $object;
private $effect;
private $statusIcon;
private $handleIcons = array();
private $bylines = array();
private $grippable;
private $actions = array();
private $headIcons = array();
private $disabled;
private $imageURI;
private $imageHref;
private $imageIcon;
private $titleText;
private $badge;
private $countdownNum;
private $countdownNoun;
private $sideColumn;
private $coverImage;
private $description;
private $selectableName;
private $selectableValue;
private $isSelected;
private $isForbidden;
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
public function addHeadIcon($icon) {
$this->headIcons[] = $icon;
return $this;
}
public function setObjectName($name) {
$this->objectName = $name;
return $this;
}
public function setGrippable($grippable) {
$this->grippable = $grippable;
return $this;
}
public function getGrippable() {
return $this->grippable;
}
public function setEffect($effect) {
$this->effect = $effect;
return $this;
}
public function getEffect() {
return $this->effect;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setSubHead($subhead) {
$this->subhead = $subhead;
return $this;
}
public function setBadge(PHUIBadgeMiniView $badge) {
$this->badge = $badge;
return $this;
}
public function setCountdown($num, $noun) {
$this->countdownNum = $num;
$this->countdownNoun = $noun;
return $this;
}
public function setTitleText($title_text) {
$this->titleText = $title_text;
return $this;
}
public function getTitleText() {
return $this->titleText;
}
public function getHeader() {
return $this->header;
}
public function addByline($byline) {
$this->bylines[] = $byline;
return $this;
}
public function setImageURI($image_uri) {
$this->imageURI = $image_uri;
return $this;
}
public function setImageHref($image_href) {
$this->imageHref = $image_href;
return $this;
}
public function getImageURI() {
return $this->imageURI;
}
public function setImageIcon($image_icon) {
if (!$image_icon instanceof PHUIIconView) {
$image_icon = id(new PHUIIconView())
->setIcon($image_icon);
}
$this->imageIcon = $image_icon;
return $this;
}
public function getImageIcon() {
return $this->imageIcon;
}
public function setCoverImage($image) {
$this->coverImage = $image;
return $this;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function setSelectable(
$name,
$value,
$is_selected,
$is_forbidden = false) {
$this->selectableName = $name;
$this->selectableValue = $value;
$this->isSelected = $is_selected;
$this->isForbidden = $is_forbidden;
return $this;
}
public function setEpoch($epoch) {
$date = phabricator_datetime($epoch, $this->getUser());
$this->addIcon('none', $date);
return $this;
}
public function addAction(PHUIListItemView $action) {
if (count($this->actions) >= 3) {
throw new Exception(pht('Limit 3 actions per item.'));
}
$this->actions[] = $action;
return $this;
}
public function addIcon($icon, $label = null, $attributes = array()) {
$this->icons[] = array(
'icon' => $icon,
'label' => $label,
'attributes' => $attributes,
);
return $this;
}
/**
* This method has been deprecated, use @{method:setImageIcon} instead.
*
* @deprecated
*/
public function setIcon($icon) {
phlog(
pht('Deprecated call to setIcon(), use setImageIcon() instead.'));
return $this->setImageIcon($icon);
}
public function setStatusIcon($icon, $label = null) {
$this->statusIcon = array(
'icon' => $icon,
'label' => $label,
);
return $this;
}
public function addHandleIcon(
PhabricatorObjectHandle $handle,
$label = null) {
$this->handleIcons[] = array(
'icon' => $handle,
'label' => $label,
);
return $this;
}
public function setBarColor($bar_color) {
$this->barColor = $bar_color;
return $this;
}
public function getBarColor() {
return $this->barColor;
}
public function addAttribute($attribute) {
if (!empty($attribute)) {
$this->attributes[] = $attribute;
}
return $this;
}
public function setSideColumn($column) {
$this->sideColumn = $column;
return $this;
}
protected function getTagName() {
return 'li';
}
protected function getTagAttributes() {
$sigils = array();
$item_classes = array();
$item_classes[] = 'phui-oi';
if ($this->icons) {
$item_classes[] = 'phui-oi-with-icons';
}
if ($this->attributes) {
$item_classes[] = 'phui-oi-with-attrs';
}
if ($this->handleIcons) {
$item_classes[] = 'phui-oi-with-handle-icons';
}
if ($this->barColor) {
$item_classes[] = 'phui-oi-bar-color-'.$this->barColor;
} else {
$item_classes[] = 'phui-oi-no-bar';
}
if ($this->actions) {
$n = count($this->actions);
$item_classes[] = 'phui-oi-with-actions';
$item_classes[] = 'phui-oi-with-'.$n.'-actions';
}
if ($this->disabled) {
$item_classes[] = 'phui-oi-disabled';
}
switch ($this->effect) {
case 'highlighted':
$item_classes[] = 'phui-oi-highlighted';
break;
case 'selected':
$item_classes[] = 'phui-oi-selected';
break;
case 'visited':
$item_classes[] = 'phui-oi-visited';
break;
case null:
break;
default:
throw new Exception(pht('Invalid effect!'));
}
if ($this->isForbidden) {
$item_classes[] = 'phui-oi-forbidden';
} else if ($this->isSelected) {
$item_classes[] = 'phui-oi-selected';
}
if ($this->selectableName !== null && !$this->isForbidden) {
$item_classes[] = 'phui-oi-selectable';
$sigils[] = 'phui-oi-selectable';
Javelin::initBehavior('phui-selectable-list');
}
if ($this->getGrippable()) {
$item_classes[] = 'phui-oi-grippable';
}
if ($this->getImageURI()) {
$item_classes[] = 'phui-oi-with-image';
}
if ($this->getImageIcon()) {
$item_classes[] = 'phui-oi-with-image-icon';
}
return array(
'class' => $item_classes,
'sigil' => $sigils,
);
}
protected function getTagContent() {
$viewer = $this->getUser();
$content_classes = array();
$content_classes[] = 'phui-oi-content';
$header_name = array();
if ($viewer) {
$header_name[] = id(new PHUISpacesNamespaceContextView())
->setUser($viewer)
->setObject($this->object);
}
if ($this->objectName) {
$header_name[] = array(
phutil_tag(
'span',
array(
'class' => 'phui-oi-objname',
),
$this->objectName),
' ',
);
}
$title_text = null;
if ($this->titleText) {
$title_text = $this->titleText;
} else if ($this->href) {
$title_text = $this->header;
}
$header_link = phutil_tag(
$this->href ? 'a' : 'div',
array(
'href' => $this->href,
'class' => 'phui-oi-link',
'title' => $title_text,
),
$this->header);
$description_tag = null;
if ($this->description) {
$decription_id = celerity_generate_unique_node_id();
$description_tag = id(new PHUITagView())
->setIcon('fa-ellipsis-h')
->addClass('phui-oi-description-tag')
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_GREY)
->addSigil('jx-toggle-class')
->setSlimShady(true)
->setMetaData(array(
'map' => array(
$decription_id => 'phui-oi-description-reveal',
),
));
}
// Wrap the header content in a <span> with the "slippery" sigil. This
// prevents us from beginning a drag if you click the text (like "T123"),
// but not if you click the white space after the header.
$header = phutil_tag(
'div',
array(
'class' => 'phui-oi-name',
),
javelin_tag(
'span',
array(
'sigil' => 'slippery',
),
array(
$this->headIcons,
$header_name,
$header_link,
$description_tag,
)));
$icons = array();
if ($this->icons) {
$icon_list = array();
foreach ($this->icons as $spec) {
$icon = $spec['icon'];
$icon = id(new PHUIIconView())
->setIcon($icon)
->addClass('phui-oi-icon-image');
if (isset($spec['attributes']['tip'])) {
$sigil = 'has-tooltip';
$meta = array(
'tip' => $spec['attributes']['tip'],
'align' => 'W',
);
$icon->addSigil($sigil);
$icon->setMetadata($meta);
}
$label = phutil_tag(
'span',
array(
'class' => 'phui-oi-icon-label',
),
$spec['label']);
if (isset($spec['attributes']['href'])) {
$icon_href = phutil_tag(
'a',
array('href' => $spec['attributes']['href']),
array($icon, $label));
} else {
$icon_href = array($icon, $label);
}
$classes = array();
$classes[] = 'phui-oi-icon';
if (isset($spec['attributes']['class'])) {
$classes[] = $spec['attributes']['class'];
}
$icon_list[] = javelin_tag(
'li',
array(
'class' => implode(' ', $classes),
),
$icon_href);
}
$icons[] = phutil_tag(
'ul',
array(
'class' => 'phui-oi-icons',
),
$icon_list);
}
$handle_bar = null;
if ($this->handleIcons) {
$handle_bar = array();
foreach ($this->handleIcons as $handleicon) {
$handle_bar[] =
$this->renderHandleIcon($handleicon['icon'], $handleicon['label']);
}
$handle_bar = phutil_tag(
'li',
array(
'class' => 'phui-oi-handle-icons',
),
$handle_bar);
}
$bylines = array();
if ($this->bylines) {
foreach ($this->bylines as $byline) {
$bylines[] = phutil_tag(
'div',
array(
'class' => 'phui-oi-byline',
),
$byline);
}
$bylines = phutil_tag(
'div',
array(
'class' => 'phui-oi-bylines',
),
$bylines);
}
$subhead = null;
if ($this->subhead) {
$subhead = phutil_tag(
'div',
array(
'class' => 'phui-oi-subhead',
),
$this->subhead);
}
if ($this->description) {
$subhead = phutil_tag(
'div',
array(
'class' => 'phui-oi-subhead phui-oi-description',
'id' => $decription_id,
),
$this->description);
}
if ($icons) {
$icons = phutil_tag(
'div',
array(
'class' => 'phui-object-icon-pane',
),
$icons);
}
$attrs = null;
if ($this->attributes || $handle_bar) {
$attrs = array();
$spacer = phutil_tag(
'span',
array(
'class' => 'phui-oi-attribute-spacer',
),
"\xC2\xB7");
$first = true;
foreach ($this->attributes as $attribute) {
$attrs[] = phutil_tag(
'li',
array(
'class' => 'phui-oi-attribute',
),
array(
($first ? null : $spacer),
$attribute,
));
$first = false;
}
$attrs = phutil_tag(
'ul',
array(
'class' => 'phui-oi-attributes',
),
array(
$handle_bar,
$attrs,
));
}
$status = null;
if ($this->statusIcon) {
$icon = $this->statusIcon;
$status = $this->renderStatusIcon($icon['icon'], $icon['label']);
}
$grippable = null;
if ($this->getGrippable()) {
$grippable = phutil_tag(
'div',
array(
'class' => 'phui-oi-grip',
),
'');
}
$content = phutil_tag(
'div',
array(
'class' => implode(' ', $content_classes),
),
array(
$subhead,
$attrs,
$this->renderChildren(),
));
$image = null;
if ($this->getImageURI()) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-oi-image',
'style' => 'background-image: url('.$this->getImageURI().')',
),
'');
} else if ($this->getImageIcon()) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-oi-image-icon',
),
$this->getImageIcon());
}
if ($image && (strlen($this->href) || strlen($this->imageHref))) {
$image_href = ($this->imageHref) ? $this->imageHref : $this->href;
$image = phutil_tag(
'a',
array(
'href' => $image_href,
),
$image);
}
/* Build a fake table */
$column0 = null;
if ($status) {
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0',
),
$status);
}
if ($this->badge) {
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0 phui-oi-badge',
),
$this->badge);
}
if ($this->countdownNum) {
$countdown = phutil_tag(
'div',
array(
'class' => 'phui-oi-countdown-number',
),
array(
phutil_tag_div('', $this->countdownNum),
phutil_tag_div('', $this->countdownNoun),
));
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0 phui-oi-countdown',
),
$countdown);
}
if ($this->selectableName !== null) {
if (!$this->isForbidden) {
$checkbox = phutil_tag(
'input',
array(
'type' => 'checkbox',
'name' => $this->selectableName,
'value' => $this->selectableValue,
'checked' => ($this->isSelected ? 'checked' : null),
));
} else {
$checkbox = null;
}
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0 phui-oi-checkbox',
),
$checkbox);
}
$column1 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col1',
),
array(
$header,
$content,
));
$column2 = null;
if ($icons || $bylines) {
$column2 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col2',
),
array(
$icons,
$bylines,
));
}
/* Fixed width, right column container. */
+ $column3 = null;
if ($this->sideColumn) {
- $column2 = phutil_tag(
+ $column3 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col2 phui-oi-side-column',
),
array(
$this->sideColumn,
));
}
$table = phutil_tag(
'div',
array(
'class' => 'phui-oi-table',
),
phutil_tag_div(
'phui-oi-table-row',
array(
$column0,
$column1,
$column2,
+ $column3,
)));
$box = phutil_tag(
'div',
array(
'class' => 'phui-oi-content-box',
),
array(
$grippable,
$table,
));
$actions = array();
if ($this->actions) {
Javelin::initBehavior('phabricator-tooltips');
foreach (array_reverse($this->actions) as $action) {
$action->setRenderNameAsTooltip(true);
$actions[] = $action;
}
$actions = phutil_tag(
'ul',
array(
'class' => 'phui-oi-actions',
),
$actions);
}
$frame_content = phutil_tag(
'div',
array(
'class' => 'phui-oi-frame-content',
),
array(
$actions,
$image,
$box,
));
$frame_cover = null;
if ($this->coverImage) {
$cover_image = phutil_tag(
'img',
array(
'src' => $this->coverImage,
'class' => 'phui-oi-cover-image',
));
$frame_cover = phutil_tag(
'div',
array(
'class' => 'phui-oi-frame-cover',
),
$cover_image);
}
$frame = phutil_tag(
'div',
array(
'class' => 'phui-oi-frame',
),
array(
$frame_cover,
$frame_content,
));
return $frame;
}
private function renderStatusIcon($icon, $label) {
Javelin::initBehavior('phabricator-tooltips');
$icon = id(new PHUIIconView())
->setIcon($icon);
$options = array(
'class' => 'phui-oi-status-icon',
);
if (strlen($label)) {
$options['sigil'] = 'has-tooltip';
$options['meta'] = array('tip' => $label, 'size' => 300);
}
return javelin_tag('div', $options, $icon);
}
private function renderHandleIcon(PhabricatorObjectHandle $handle, $label) {
Javelin::initBehavior('phabricator-tooltips');
$options = array(
'class' => 'phui-oi-handle-icon',
'style' => 'background-image: url('.$handle->getImageURI().')',
);
if (strlen($label)) {
$options['sigil'] = 'has-tooltip';
$options['meta'] = array('tip' => $label, 'align' => 'E');
}
return javelin_tag('span', $options, '');
}
}
diff --git a/src/view/phui/PHUIRemarkupPreviewPanel.php b/src/view/phui/PHUIRemarkupPreviewPanel.php
index 064379cd0..46c0f5cd5 100644
--- a/src/view/phui/PHUIRemarkupPreviewPanel.php
+++ b/src/view/phui/PHUIRemarkupPreviewPanel.php
@@ -1,118 +1,118 @@
<?php
/**
* Render a simple preview panel for a bound Remarkup text control.
*/
final class PHUIRemarkupPreviewPanel extends AphrontTagView {
private $header;
private $loadingText;
private $controlID;
private $previewURI;
private $previewType;
const DOCUMENT = 'document';
protected function canAppendChild() {
return false;
}
public function setPreviewURI($preview_uri) {
$this->previewURI = $preview_uri;
return $this;
}
public function setControlID($control_id) {
$this->controlID = $control_id;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setLoadingText($loading_text) {
$this->loadingText = $loading_text;
return $this;
}
public function setPreviewType($type) {
$this->previewType = $type;
return $this;
}
protected function getTagName() {
return 'div';
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phui-remarkup-preview';
return array(
'class' => $classes,
);
}
protected function getTagContent() {
if ($this->previewURI === null) {
throw new PhutilInvalidStateException('setPreviewURI');
}
if ($this->controlID === null) {
throw new PhutilInvalidStateException('setControlID');
}
$preview_id = celerity_generate_unique_node_id();
require_celerity_resource('phui-remarkup-preview-css');
Javelin::initBehavior(
'remarkup-preview',
array(
'previewID' => $preview_id,
'controlID' => $this->controlID,
'uri' => $this->previewURI,
));
$loading = phutil_tag(
'div',
array(
'class' => 'phui-preview-loading-text',
),
nonempty($this->loadingText, pht('Loading preview...')));
$preview = phutil_tag(
'div',
array(
'id' => $preview_id,
'class' => 'phabricator-remarkup phui-preview-body',
),
$loading);
if (!$this->previewType) {
$header = null;
if ($this->header) {
$header = phutil_tag(
'div',
array(
'class' => 'phui-preview-header',
),
$this->header);
}
$content = array($header, $preview);
} else if ($this->previewType == self::DOCUMENT) {
$header = id(new PHUIHeaderView())
->setHeader(pht('%s (Preview)', $this->header));
- $content = id(new PHUIDocumentViewPro())
+ $content = id(new PHUIDocumentView())
->setHeader($header)
->appendChild($preview);
}
return id(new PHUIObjectBoxView())
->appendChild($content)
->setCollapsed(true);
}
}
diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php
index e64b9b06b..e44332c1b 100644
--- a/src/view/phui/PHUITimelineEventView.php
+++ b/src/view/phui/PHUITimelineEventView.php
@@ -1,708 +1,708 @@
<?php
final class PHUITimelineEventView extends AphrontView {
const DELIMITER = " \xC2\xB7 ";
private $userHandle;
private $title;
private $icon;
private $color;
private $classes = array();
private $contentSource;
private $dateCreated;
private $anchor;
private $isEditable;
private $isEdited;
private $isRemovable;
private $transactionPHID;
private $isPreview;
private $eventGroup = array();
private $hideByDefault;
private $token;
private $tokenRemoved;
private $quoteTargetID;
private $isNormalComment;
private $quoteRef;
private $reallyMajorEvent;
private $hideCommentOptions = false;
private $authorPHID;
private $badges = array();
private $pinboardItems = array();
private $isSilent;
public function setAuthorPHID($author_phid) {
$this->authorPHID = $author_phid;
return $this;
}
public function getAuthorPHID() {
return $this->authorPHID;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setIsNormalComment($is_normal_comment) {
$this->isNormalComment = $is_normal_comment;
return $this;
}
public function getIsNormalComment() {
return $this->isNormalComment;
}
public function setHideByDefault($hide_by_default) {
$this->hideByDefault = $hide_by_default;
return $this;
}
public function getHideByDefault() {
return $this->hideByDefault;
}
public function setTransactionPHID($transaction_phid) {
$this->transactionPHID = $transaction_phid;
return $this;
}
public function getTransactionPHID() {
return $this->transactionPHID;
}
public function setIsEdited($is_edited) {
$this->isEdited = $is_edited;
return $this;
}
public function getIsEdited() {
return $this->isEdited;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsEditable($is_editable) {
$this->isEditable = $is_editable;
return $this;
}
public function getIsEditable() {
return $this->isEditable;
}
public function setIsRemovable($is_removable) {
$this->isRemovable = $is_removable;
return $this;
}
public function getIsRemovable() {
return $this->isRemovable;
}
public function setDateCreated($date_created) {
$this->dateCreated = $date_created;
return $this;
}
public function getDateCreated() {
return $this->dateCreated;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function setUserHandle(PhabricatorObjectHandle $handle) {
$this->userHandle = $handle;
return $this;
}
public function setAnchor($anchor) {
$this->anchor = $anchor;
return $this;
}
public function getAnchor() {
return $this->anchor;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function addBadge(PHUIBadgeMiniView $badge) {
$this->badges[] = $badge;
return $this;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function setIsSilent($is_silent) {
$this->isSilent = $is_silent;
return $this;
}
public function getIsSilent() {
return $this->isSilent;
}
public function setReallyMajorEvent($me) {
$this->reallyMajorEvent = $me;
return $this;
}
public function setHideCommentOptions($hide_comment_options) {
$this->hideCommentOptions = $hide_comment_options;
return $this;
}
public function getHideCommentOptions() {
return $this->hideCommentOptions;
}
public function addPinboardItem(PHUIPinboardItemView $item) {
$this->pinboardItems[] = $item;
return $this;
}
public function setToken($token, $removed = false) {
$this->token = $token;
$this->tokenRemoved = $removed;
return $this;
}
public function getEventGroup() {
return array_merge(array($this), $this->eventGroup);
}
public function addEventToGroup(PHUITimelineEventView $event) {
$this->eventGroup[] = $event;
return $this;
}
protected function shouldRenderEventTitle() {
if ($this->title === null) {
return false;
}
return true;
}
protected function renderEventTitle($force_icon, $has_menu, $extra) {
$title = $this->title;
$title_classes = array();
$title_classes[] = 'phui-timeline-title';
$icon = null;
if ($this->icon || $force_icon) {
$title_classes[] = 'phui-timeline-title-with-icon';
}
if ($has_menu) {
$title_classes[] = 'phui-timeline-title-with-menu';
}
if ($this->icon) {
$fill_classes = array();
$fill_classes[] = 'phui-timeline-icon-fill';
if ($this->color) {
$fill_classes[] = 'fill-has-color';
$fill_classes[] = 'phui-timeline-icon-fill-'.$this->color;
}
$icon = id(new PHUIIconView())
->setIcon($this->icon)
->addClass('phui-timeline-icon');
$icon = phutil_tag(
'span',
array(
'class' => implode(' ', $fill_classes),
),
$icon);
}
$token = null;
if ($this->token) {
$token = id(new PHUIIconView())
->addClass('phui-timeline-token')
->setSpriteSheet(PHUIIconView::SPRITE_TOKENS)
->setSpriteIcon($this->token);
if ($this->tokenRemoved) {
$token->addClass('strikethrough');
}
}
$title = phutil_tag(
'div',
array(
'class' => implode(' ', $title_classes),
),
array($icon, $token, $title, $extra));
return $title;
}
public function render() {
$events = $this->getEventGroup();
// Move events with icons first.
$icon_keys = array();
foreach ($this->getEventGroup() as $key => $event) {
if ($event->icon) {
$icon_keys[] = $key;
}
}
$events = array_select_keys($events, $icon_keys) + $events;
$force_icon = (bool)$icon_keys;
$menu = null;
$items = array();
if (!$this->getIsPreview() && !$this->getHideCommentOptions()) {
foreach ($this->getEventGroup() as $event) {
$items[] = $event->getMenuItems($this->anchor);
}
$items = array_mergev($items);
}
if ($items) {
$icon = id(new PHUIIconView())
->setIcon('fa-caret-down');
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
pht('Comment Actions'));
if ($items) {
$sigil = 'phui-dropdown-menu';
Javelin::initBehavior('phui-dropdown-menu');
} else {
$sigil = null;
}
$action_list = id(new PhabricatorActionListView())
->setUser($this->getUser());
foreach ($items as $item) {
$action_list->addAction($item);
}
$menu = javelin_tag(
$items ? 'a' : 'span',
array(
'href' => '#',
'class' => 'phui-timeline-menu',
'sigil' => $sigil,
'aria-haspopup' => 'true',
'aria-expanded' => 'false',
'meta' => $action_list->getDropdownMenuMetadata(),
),
array(
$aural,
$icon,
));
$has_menu = true;
} else {
$has_menu = false;
}
// Render "extra" information (timestamp, etc).
$extra = $this->renderExtra($events);
$show_badges = false;
$group_titles = array();
$group_items = array();
$group_children = array();
foreach ($events as $event) {
if ($event->shouldRenderEventTitle()) {
// Render the group anchor here, outside the title box. If we render
// it inside the title box it ends up completely hidden and Chrome 55
// refuses to jump to it. See T11997 for discussion.
if ($extra && $this->anchor) {
$group_titles[] = id(new PhabricatorAnchorView())
->setAnchorName($this->anchor)
->render();
}
$group_titles[] = $event->renderEventTitle(
$force_icon,
$has_menu,
$extra);
// Don't render this information more than once.
$extra = null;
}
if ($event->hasChildren()) {
$group_children[] = $event->renderChildren();
$show_badges = true;
}
}
$image_uri = $this->userHandle->getImageURI();
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge phui-timeline-border',
'style' => (nonempty($image_uri)) ? '' : 'display: none;',
),
'');
$image = null;
$badges = null;
if ($image_uri) {
$image = phutil_tag(
($this->userHandle->getURI()) ? 'a' : 'div',
array(
'style' => 'background-image: url('.$image_uri.')',
- 'class' => 'phui-timeline-image',
+ 'class' => 'phui-timeline-image visual-only',
'href' => $this->userHandle->getURI(),
),
'');
if ($this->badges && $show_badges) {
$flex = new PHUIBadgeBoxView();
$flex->addItems($this->badges);
$flex->setCollapsed(true);
$badges = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
}
$content_classes = array();
$content_classes[] = 'phui-timeline-content';
$classes = array();
$classes[] = 'phui-timeline-event-view';
if ($group_children) {
$classes[] = 'phui-timeline-major-event';
$content = phutil_tag(
'div',
array(
'class' => 'phui-timeline-inner-content',
),
array(
$group_titles,
$menu,
phutil_tag(
'div',
array(
'class' => 'phui-timeline-core-content',
),
$group_children),
));
} else {
$classes[] = 'phui-timeline-minor-event';
$content = $group_titles;
}
$content = phutil_tag(
'div',
array(
'class' => 'phui-timeline-group phui-timeline-border',
),
$content);
// Image Events
$pinboard = null;
if ($this->pinboardItems) {
$pinboard = new PHUIPinboardView();
foreach ($this->pinboardItems as $item) {
$pinboard->addItem($item);
}
}
$content = phutil_tag(
'div',
array(
'class' => implode(' ', $content_classes),
),
array($image, $badges, $wedge, $content, $pinboard));
$outer_classes = $this->classes;
$outer_classes[] = 'phui-timeline-shell';
$color = null;
foreach ($this->getEventGroup() as $event) {
if ($event->color) {
$color = $event->color;
break;
}
}
if ($color) {
$outer_classes[] = 'phui-timeline-'.$color;
}
$sigil = null;
$meta = null;
if ($this->getTransactionPHID()) {
$sigil = 'transaction';
$meta = array(
'phid' => $this->getTransactionPHID(),
'anchor' => $this->anchor,
);
}
$major_event = null;
if ($this->reallyMajorEvent) {
$major_event = phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'phui-timeline-spacer '.
'phui-timeline-spacer-bold',
'',
));
}
return array(
javelin_tag(
'div',
array(
'class' => implode(' ', $outer_classes),
'id' => $this->anchor ? 'anchor-'.$this->anchor : null,
'sigil' => $sigil,
'meta' => $meta,
),
phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$content)),
$major_event,
);
}
private function renderExtra(array $events) {
$extra = array();
if ($this->getIsPreview()) {
$extra[] = pht('PREVIEW');
} else {
foreach ($events as $event) {
if ($event->getIsEdited()) {
$extra[] = pht('Edited');
break;
}
}
$source = $this->getContentSource();
$content_source = null;
if ($source) {
$content_source = id(new PhabricatorContentSourceView())
->setContentSource($source)
->setUser($this->getUser());
$content_source = pht('Via %s', $content_source->getSourceName());
}
$date_created = null;
foreach ($events as $event) {
if ($event->getDateCreated()) {
if ($date_created === null) {
$date_created = $event->getDateCreated();
} else {
$date_created = min($event->getDateCreated(), $date_created);
}
}
}
if ($date_created) {
$date = phabricator_datetime(
$date_created,
$this->getUser());
if ($this->anchor) {
Javelin::initBehavior('phabricator-watch-anchor');
Javelin::initBehavior('phabricator-tooltips');
$date = array(
javelin_tag(
'a',
array(
'href' => '#'.$this->anchor,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $content_source,
),
),
$date),
);
}
$extra[] = $date;
}
// If this edit was applied silently, give user a hint that they should
// not expect to have received any mail or notifications.
if ($this->getIsSilent()) {
$extra[] = id(new PHUIIconView())
->setIcon('fa-bell-slash', 'red')
->setTooltip(pht('Silent Edit'));
}
}
$extra = javelin_tag(
'span',
array(
'class' => 'phui-timeline-extra',
),
phutil_implode_html(
javelin_tag(
'span',
array(
'aural' => false,
),
self::DELIMITER),
$extra));
return $extra;
}
private function getMenuItems($anchor) {
$xaction_phid = $this->getTransactionPHID();
$items = array();
if ($this->getIsEditable()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref('/transactions/edit/'.$xaction_phid.'/')
->setName(pht('Edit Comment'))
->addSigil('transaction-edit')
->setMetadata(
array(
'anchor' => $anchor,
));
}
if ($this->getQuoteTargetID()) {
$ref = null;
if ($this->getQuoteRef()) {
$ref = $this->getQuoteRef();
if ($anchor) {
$ref = $ref.'#'.$anchor;
}
}
$items[] = id(new PhabricatorActionView())
->setIcon('fa-quote-left')
->setName(pht('Quote Comment'))
->setHref('#')
->addSigil('transaction-quote')
->setMetadata(
array(
'targetID' => $this->getQuoteTargetID(),
'uri' => '/transactions/quote/'.$xaction_phid.'/',
'ref' => $ref,
));
}
if ($this->getIsNormalComment()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-code')
->setHref('/transactions/raw/'.$xaction_phid.'/')
->setName(pht('View Remarkup'))
->addSigil('transaction-raw')
->setMetadata(
array(
'anchor' => $anchor,
));
$content_source = $this->getContentSource();
$source_email = PhabricatorEmailContentSource::SOURCECONST;
if ($content_source->getSource() == $source_email) {
$source_id = $content_source->getContentSourceParameter('id');
if ($source_id) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-envelope-o')
->setHref('/transactions/raw/'.$xaction_phid.'/?email')
->setName(pht('View Email Body'))
->addSigil('transaction-raw')
->setMetadata(
array(
'anchor' => $anchor,
));
}
}
}
if ($this->getIsEdited()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-list')
->setHref('/transactions/history/'.$xaction_phid.'/')
->setName(pht('View Edit History'))
->setWorkflow(true);
}
if ($this->getIsRemovable()) {
$items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-trash-o')
->setHref('/transactions/remove/'.$xaction_phid.'/')
->setName(pht('Remove Comment'))
->setColor(PhabricatorActionView::RED)
->addSigil('transaction-remove')
->setMetadata(
array(
'anchor' => $anchor,
));
}
return $items;
}
}
diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js
index f162e4937..34bddf4fa 100644
--- a/support/aphlict/server/aphlict_server.js
+++ b/support/aphlict/server/aphlict_server.js
@@ -1,204 +1,199 @@
'use strict';
var JX = require('./lib/javelin').JX;
var http = require('http');
var https = require('https');
var util = require('util');
var fs = require('fs');
function parse_command_line_arguments(argv) {
var args = {
test: false,
config: null
};
for (var ii = 2; ii < argv.length; ii++) {
var arg = argv[ii];
var matches = arg.match(/^--([^=]+)=(.*)$/);
if (!matches) {
throw new Error('Unknown argument "' + arg + '"!');
}
if (!(matches[1] in args)) {
throw new Error('Unknown argument "' + matches[1] + '"!');
}
args[matches[1]] = matches[2];
}
return args;
}
function parse_config(args) {
var data = fs.readFileSync(args.config);
return JSON.parse(data);
}
require('./lib/AphlictLog');
var debug = new JX.AphlictLog()
.addConsole(console);
var args = parse_command_line_arguments(process.argv);
var config = parse_config(args);
function set_exit_code(code) {
process.on('exit', function() {
process.exit(code);
});
}
process.on('uncaughtException', function(err) {
var context = null;
if (err.code == 'EACCES') {
context = util.format(
'Unable to open file ("%s"). Check that permissions are set ' +
'correctly.',
err.path);
}
var message = [
'\n<<< UNCAUGHT EXCEPTION! >>>',
];
if (context) {
message.push(context);
}
message.push(err.stack);
debug.log(message.join('\n\n'));
set_exit_code(1);
});
try {
require('ws');
} catch (ex) {
throw new Error(
'You need to install the Node.js "ws" module for websocket support. ' +
'See "Notifications User Guide: Setup and Configuration" in the ' +
'documentation for instructions. ' + ex.toString());
}
// NOTE: Require these only after checking for the "ws" module, since they
// depend on it.
require('./lib/AphlictAdminServer');
require('./lib/AphlictClientServer');
require('./lib/AphlictPeerList');
require('./lib/AphlictPeer');
var ii;
var logs = config.logs || [];
for (ii = 0; ii < logs.length; ii++) {
debug.addLog(logs[ii].path);
}
var servers = [];
for (ii = 0; ii < config.servers.length; ii++) {
var spec = config.servers[ii];
spec.listen = spec.listen || '0.0.0.0';
if (spec['ssl.key']) {
spec['ssl.key'] = fs.readFileSync(spec['ssl.key']);
}
if (spec['ssl.cert']){
spec['ssl.cert'] = fs.readFileSync(spec['ssl.cert']);
- }
-
- if (spec['ssl.chain']){
- spec['ssl.chain'] = fs.readFileSync(spec['ssl.chain']);
+ if (spec['ssl.chain']){
+ spec['ssl.cert'] += "\n" + fs.readFileSync(spec['ssl.chain']);
+ }
}
servers.push(spec);
}
// If we're just doing a configuration test, exit here before starting any
// servers.
if (args.test) {
debug.log('Configuration test OK.');
set_exit_code(0);
return;
}
debug.log('Starting servers (service PID %d).', process.pid);
for (ii = 0; ii < logs.length; ii++) {
debug.log('Logging to "%s".', logs[ii].path);
}
var aphlict_servers = [];
var aphlict_clients = [];
var aphlict_admins = [];
for (ii = 0; ii < servers.length; ii++) {
var server = servers[ii];
var is_client = (server.type == 'client');
var http_server;
if (server['ssl.key']) {
var https_config = {
key: server['ssl.key'],
cert: server['ssl.cert'],
};
- if (server['ssl.chain']) {
- https_config.ca = server['ssl.chain'];
- }
-
http_server = https.createServer(https_config);
} else {
http_server = http.createServer();
}
var aphlict_server;
if (is_client) {
aphlict_server = new JX.AphlictClientServer(http_server);
} else {
aphlict_server = new JX.AphlictAdminServer(http_server);
}
aphlict_server.setLogger(debug);
aphlict_server.listen(server.port, server.listen);
debug.log(
'Started %s server (Port %d, %s).',
server.type,
server.port,
server['ssl.key'] ? 'With SSL' : 'No SSL');
aphlict_servers.push(aphlict_server);
if (is_client) {
aphlict_clients.push(aphlict_server);
} else {
aphlict_admins.push(aphlict_server);
}
}
var peer_list = new JX.AphlictPeerList();
debug.log(
'This server has fingerprint "%s".',
peer_list.getFingerprint());
var cluster = config.cluster || [];
for (ii = 0; ii < cluster.length; ii++) {
var peer = cluster[ii];
var peer_client = new JX.AphlictPeer()
.setHost(peer.host)
.setPort(peer.port)
.setProtocol(peer.protocol);
peer_list.addPeer(peer_client);
}
for (ii = 0; ii < aphlict_admins.length; ii++) {
var admin_server = aphlict_admins[ii];
admin_server.setClientServers(aphlict_clients);
admin_server.setPeerList(peer_list);
}
for (ii = 0; ii < aphlict_clients.length; ii++) {
var client_server = aphlict_clients[ii];
client_server.setAdminServers(aphlict_admins);
}
diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css
index 8e2391d57..8db243689 100644
--- a/webroot/rsrc/css/application/base/notification-menu.css
+++ b/webroot/rsrc/css/application/base/notification-menu.css
@@ -1,160 +1,164 @@
/**
* @provides phabricator-notification-menu-css
*/
.phabricator-notification-menu {
background: {$page.content};
font-size: {$smallerfontsize};
line-height: 18px;
word-wrap: break-word;
overflow-y: auto;
box-shadow: {$dropshadow};
border: 1px solid {$lightgreyborder};
border-radius: 3px;
}
.phabricator-notification {
padding: 8px 12px;
}
.phabricator-notification-menu-loading {
text-align: center;
padding: 10px 0;
color: {$lightgreytext};
}
.device-desktop .phabricator-notification-menu,
.device-tablet .phabricator-notification-menu {
position: absolute;
width: 360px;
top: 42px;
}
.device-phone .phabricator-notification-menu {
border-bottom: 1px solid {$thinblueborder};
width: 94%;
max-width: 390px;
top: 42px !important;
left: 3% !important;
position: absolute;
}
.phabricator-notification-list.pm {
padding: 0;
}
.phabricator-notification-list .phabricator-notification {
padding: 8px;
}
.phabricator-notification-menu .phabricator-notification {
cursor: pointer;
}
.device-desktop .phabricator-notification-menu .phabricator-notification:hover {
background: {$lightgreybackground};
}
.device-desktop .phabricator-notification-menu
.phabricator-notification-unread.phabricator-notification:hover {
background: {$hoverselectedblue};
}
.phabricator-notification + .phabricator-notification {
border-top: 1px solid {$thinblueborder};
}
.no-notifications {
color: {$lightgreytext};
}
+.phabricator-notification-warning {
+ background: {$sh-yellowbackground};
+}
+
.phabricator-notification-list .phabricator-notification-unread,
.phabricator-notification-menu .phabricator-notification-unread {
background: {$hoverblue};
}
.phabricator-notification-read {
color: {$lightgreytext};
}
.phabricator-notification-foot {
color: {$lightgreytext};
font-size: {$smallestfontsize};
line-height: 18px;
position: relative;
}
.phabricator-notification-unread .phabricator-notification-foot {
padding-left: 10px;
}
.phabricator-notification-foot .phabricator-notification-status {
display: none;
}
.phabricator-notification-unread .phabricator-notification-foot
.phabricator-notification-status {
font-size: 7px;
- color: {$lightgreytext};
+ color: {$lightbluetext};
position: absolute;
display: inline-block;
top: 6px;
left: 0;
}
.phabricator-notification-header {
font-weight: bold;
padding: 10px 12px;
font-size: {$smallerfontsize};
border-bottom: 1px solid {$thinblueborder};
}
.phabricator-notification-header a {
color: {$darkgreytext};
}
.phabricator-notification-header a:hover {
text-decoration: underline;
}
.phabricator-notification-header .phabricator-notification-clear-all {
color: {$anchor};
float: right;
font-weight: normal;
}
.phabricator-notification-footer {
background: {$greybackground};
border-top: 1px solid {$thinblueborder};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
padding: 8px;
font-size: {$smallerfontsize};
color: {$darkgreytext};
}
.phabricator-notification-footer a {
color: {$darkgreytext};
}
.phabricator-notification-footer a:hover {
text-decoration: underline;
}
.phabricator-notification-menu .aphlict-connection-status {
color: {$lightgreytext};
}
.aphlict-connection-status {
position: relative;
}
.aphlict-connection-status .phui-icon-view {
font-size: 9px;
position: absolute;
top: 4px;
}
.aphlict-connection-status .connection-status-text {
margin-left: 12px;
}
diff --git a/webroot/rsrc/css/application/harbormaster/harbormaster.css b/webroot/rsrc/css/application/harbormaster/harbormaster.css
index f3b75a397..9fdad43a4 100644
--- a/webroot/rsrc/css/application/harbormaster/harbormaster.css
+++ b/webroot/rsrc/css/application/harbormaster/harbormaster.css
@@ -1,169 +1,172 @@
/**
* @provides harbormaster-css
*/
.harbormaster-artifact-io {
margin: 4px 0 4px 8px;
padding: 8px;
}
.harbormaster-artifact-summary-header {
font-weight: bold;
margin-bottom: 2px;
color: {$darkbluetext};
}
.harbormaster-empty-logs-are-hidden {
background: {$lightyellow};
border: 1px solid {$yellow};
text-align: center;
padding: 12px;
margin: 0 0 20px 0;
border-radius: 3px;
color: {$darkgreytext};
}
.harbormaster-unit-details {
margin: 8px 0 4px;
overflow: hidden;
- white-space: pre;
- text-overflow: ellipsis;
color: {$lightgreytext};
}
+.harbormaster-unit-details-text {
+ white-space: pre-wrap;
+ text-overflow: ellipsis;
+}
+
.harbormaster-log-view-loading {
padding: 8px;
text-align: center;
color: {$lightgreytext};
}
.harbormaster-log-table > tbody > tr > th {
background-color: {$paste.highlight};
border-right: 1px solid {$paste.border};
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.harbormaster-log-table > tbody > tr > th a::before {
/* Render the line numbers into the document using a pseudo-element so that
the text is not copied. */
content: attr(data-n);
}
.harbormaster-log-table > tbody > tr > th a {
display: block;
color: {$darkbluetext};
text-align: right;
padding: 2px 6px 1px 12px;
}
.harbormaster-log-table > tbody > tr > th a:hover {
background: {$paste.border};
}
.harbormaster-log-table > tbody > tr > td {
white-space: pre-wrap;
padding: 2px 8px 1px;
width: 100%;
}
.harbormaster-log-table > tbody > tr > td.harbormaster-log-expand-cell {
padding: 4px 0;
}
.harbormaster-log-table tr.phabricator-source-highlight > th {
background: {$paste.border};
}
.harbormaster-log-table tr.phabricator-source-highlight > td {
background: {$paste.highlight};
}
.harbormaster-log-expand-table {
width: 100%;
background: {$paste.highlight};
border-top: 1px solid {$paste.border};
border-bottom: 1px solid {$paste.border};
}
.harbormaster-log-expand-table a {
display: block;
padding: 6px 16px;
color: {$darkbluetext};
}
.device-desktop .harbormaster-log-expand-table a:hover {
background: {$paste.border};
text-decoration: none;
}
.harbormaster-log-expand-table td {
vertical-align: middle;
font: 13px 'Segoe UI', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Lato',
'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.harbormaster-log-expand-up {
text-align: right;
width: 50%;
}
.harbormaster-log-expand-up .phui-icon-view {
margin: 0 0 0px 4px;
}
.harbormaster-log-follow {
text-align: center;
}
.harbormaster-log-follow .phui-icon-view {
margin: 0 4px;
}
.harbormaster-log-expand-mid {
text-align: center;
white-space: nowrap;
border-left: 1px solid {$paste.border};
border-right: 1px solid {$paste.border};
}
.harbormaster-log-expand-down {
text-align: left;
width: 50%;
}
.harbormaster-log-expand-down .phui-icon-view {
margin: 0 4px 0 0;
}
.harbormaster-log-following .harbormaster-log-table
.harbormaster-log-follow-start {
display: none;
}
.harbormaster-log-table .harbormaster-log-follow-stop {
display: none;
}
.harbormaster-log-following .harbormaster-log-table
.harbormaster-log-follow-stop {
display: block;
}
.harbormaster-log-appear > td {
animation: harbormaster-fade-in 1s linear;
}
@keyframes harbormaster-fade-in {
0% {
opacity: 0.5;
}
100% {
opacity: 1.0;
}
}
diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css
index e74bb8ad1..53e2043a1 100644
--- a/webroot/rsrc/css/core/remarkup.css
+++ b/webroot/rsrc/css/core/remarkup.css
@@ -1,922 +1,922 @@
/**
* @provides phabricator-remarkup-css
* @requires katex-css
*/
.phabricator-remarkup {
line-height: 1.7em;
word-break: break-word;
}
.phabricator-remarkup p {
margin: 0 0 12px;
}
.PhabricatorMonospaced,
.phabricator-remarkup .remarkup-code-block .remarkup-code {
font: 11px/15px "Menlo", "Consolas", "Monaco", monospace;
}
.platform-windows .PhabricatorMonospaced,
.platform-windows .phabricator-remarkup .remarkup-code-block .remarkup-code {
font: 12px/15px "Menlo", "Consolas", "Monaco", monospace;
}
.phabricator-remarkup .remarkup-code-block {
margin: 12px 0;
white-space: pre;
}
.phabricator-remarkup .remarkup-code-header {
padding: 6px 12px;
font-size: 13px;
font-weight: bold;
background: rgba({$alphablue},0.08);
display: table-cell;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
overflow: hidden;
}
.phabricator-remarkup .code-block-counterexample .remarkup-code-header {
background-color: {$sh-redbackground};
}
.phabricator-remarkup .remarkup-code-block .remarkup-code-header + pre {
border-top-left-radius: 0;
}
.phabricator-remarkup .remarkup-code-block pre {
background: rgba({$alphablue},0.08);
display: block;
color: {$blacktext};
overflow: auto;
padding: 12px;
border-radius: 3px;
white-space: pre-wrap;
}
.phabricator-remarkup kbd {
display: inline-block;
min-width: 1em;
padding: 4px 5px 5px;
font-weight: normal;
font-size: 0.8rem;
text-align: center;
text-decoration: none;
line-height: 0.6rem;
border-radius: 3px;
box-shadow: inset 0 -1px 0 rgba({$alphablue},0.08);
user-select: none;
background: {$lightgreybackground};
border: 1px solid {$lightgreyborder};
}
.phabricator-remarkup .kbd-join {
padding: 0 4px;
color: {$lightgreytext};
}
.phabricator-remarkup pre.remarkup-counterexample {
background-color: {$sh-redbackground};
}
.phabricator-remarkup tt.remarkup-monospaced {
color: {$blacktext};
background: rgba({$alphablue},0.1);
padding: 1px 4px;
border-radius: 3px;
white-space: pre-wrap;
}
/* NOTE: You can currently produce this with [[link | `name`]]. Restore the
link color. */
.phabricator-remarkup a tt.remarkup-monospaced {
color: {$anchor};
}
.phabricator-remarkup .remarkup-header tt.remarkup-monospaced {
font-weight: normal;
}
.phabricator-remarkup ul.remarkup-list {
list-style: disc;
margin: 12px 0 12px 30px;
}
.phabricator-remarkup ol.remarkup-list {
list-style: decimal;
margin: 12px 0 12px 30px;
}
.phabricator-remarkup ol ol.remarkup-list {
list-style: upper-alpha;
}
.phabricator-remarkup ol ol ol.remarkup-list {
list-style: lower-alpha;
}
.phabricator-remarkup ol ol ol ol.remarkup-list {
list-style: lower-roman;
}
.phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-checked-item,
.phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-unchecked-item {
list-style: none;
margin-left: -20px;
position: relative;
padding-left: 22px;
}
.phabricator-remarkup .remarkup-list-with-checkmarks input {
visibility: hidden;
width: 0;
}
.phabricator-remarkup .remarkup-list-with-checkmarks
.remarkup-list-item::before {
height: 16px;
width: 16px;
background-size: 16px;
display: inline-block;
content: '';
position: absolute;
top: 3px;
left: 0;
}
.remarkup-list-with-checkmarks .remarkup-checked-item::before {
background-image: url(rsrc/image/controls/checkbox-checked.png);
}
.remarkup-list-with-checkmarks .remarkup-unchecked-item::before {
background-image: url(rsrc/image/controls/checkbox-unchecked.png);
}
.phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-checked-item {
color: {$lightgreytext};
text-decoration: line-through;
}
.phabricator-remarkup ul.remarkup-list ol.remarkup-list,
.phabricator-remarkup ul.remarkup-list ul.remarkup-list,
.phabricator-remarkup ol.remarkup-list ol.remarkup-list,
.phabricator-remarkup ol.remarkup-list ul.remarkup-list {
margin: 4px 0 4px 24px;
}
.phabricator-remarkup .remarkup-list-item {
line-height: 1.7em;
}
.phabricator-remarkup li.phantom-item,
.phabricator-remarkup li.phantom-item {
list-style-type: none;
}
.phabricator-remarkup h1.remarkup-header {
font-size: 24px;
line-height: 1.625em;
margin: 24px 0 4px;
}
.phabricator-remarkup h2.remarkup-header {
font-size: 20px;
line-height: 1.5em;
margin: 20px 0 4px;
}
.phabricator-remarkup h3.remarkup-header {
font-size: 18px;
line-height: 1.375em;
margin: 20px 0 4px;
}
.phabricator-remarkup h4.remarkup-header {
font-size: 16px;
line-height: 1.25em;
margin: 12px 0 4px;
}
.phabricator-remarkup h5.remarkup-header {
font-size: 15px;
line-height: 1.125em;
margin: 8px 0 4px;
}
.phabricator-remarkup h6.remarkup-header {
font-size: 14px;
line-height: 1em;
margin: 4px 0;
}
.phabricator-remarkup blockquote {
border-left: 3px solid {$sh-blueborder};
color: {$darkbluetext};
font-style: italic;
margin: 4px 0 12px 0;
padding: 8px 12px;
background-color: {$lightbluebackground};
}
.phabricator-remarkup blockquote *:last-child {
margin-bottom: 0;
}
.phabricator-remarkup blockquote blockquote {
background-color: rgba(175,175,175, .1);
}
.phabricator-remarkup blockquote em {
/* In blockquote bodies, default text is italic so emphasized text should
be normal. */
font-style: normal;
}
.phabricator-remarkup blockquote div.remarkup-reply-head {
font-style: normal;
padding-bottom: 4px;
}
.phabricator-remarkup blockquote div.remarkup-reply-head em {
/* In blockquote headers, default text is normal so emphasized text should
be italic. See T10686. */
font-style: italic;
}
.phabricator-remarkup blockquote div.remarkup-reply-head
.phui-tag-core {
background-color: transparent;
border: none;
padding: 0;
color: {$darkbluetext};
}
.phabricator-remarkup img.remarkup-proxy-image {
max-width: 640px;
max-height: 640px;
}
.phabricator-remarkup audio {
display: block;
margin: 16px auto;
min-width: 240px;
width: 50%;
}
video.phabricator-media {
background: {$greybackground};
}
.phabricator-remarkup video {
display: block;
margin: 0 auto;
max-width: 95%;
}
.phabricator-remarkup-mention-exists {
font-weight: bold;
background: #e6f3ff;
}
.phabricator-remarkup-mention-disabled {
font-weight: bold;
background: #dddddd;
}
.phui-remarkup-preview .phabricator-remarkup-mention-unknown,
.aphront-panel-preview .phabricator-remarkup-mention-unknown {
font-weight: bold;
background: #ffaaaa;
}
.phabricator-remarkup .phriction-link {
font-weight: bold;
}
.phabricator-remarkup .phriction-link-missing {
color: {$red};
text-decoration: underline;
}
.phabricator-remarkup .phriction-link-lock {
color: {$greytext};
}
.phabricator-remarkup-mention-nopermission .phui-tag-core {
background: {$lightgreybackground};
color: {$lightgreytext};
}
.phabricator-remarkup .remarkup-note {
margin: 16px 0;
padding: 12px;
border-left: 3px solid {$blue};
background: {$lightblue};
}
.phabricator-remarkup .remarkup-warning {
margin: 16px 0;
padding: 12px;
border-left: 3px solid {$yellow};
background: {$lightyellow};
}
.phabricator-remarkup .remarkup-important {
margin: 16px 0;
padding: 12px;
border-left: 3px solid {$red};
background: {$lightred};
}
.phabricator-remarkup .remarkup-note .remarkup-monospaced,
.phabricator-remarkup .remarkup-important .remarkup-monospaced,
.phabricator-remarkup .remarkup-warning .remarkup-monospaced {
background-color: rgba(150,150,150,.2);
}
.phabricator-remarkup .remarkup-note-word {
font-weight: bold;
color: {$darkbluetext};
}
.phabricator-remarkup-toc {
float: right;
border-left: 1px solid {$lightblueborder};
background: {$page.content};
width: 160px;
padding-left: 8px;
margin: 0 0 4px 8px;
}
.phabricator-remarkup-toc-header {
font-size: 13px;
line-height: 13px;
color: {$darkbluetext};
font-weight: bold;
margin-bottom: 4px;
}
.phabricator-remarkup-toc ul {
padding: 0;
margin: 0;
list-style: none;
overflow: hidden;
}
.phabricator-remarkup-toc ul ul {
margin: 0 0 0 8px;
}
.phabricator-remarkup-toc ul li {
padding: 0;
margin: 0;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.phabricator-remarkup-embed-layout-right {
text-align: right;
}
.phabricator-remarkup-embed-layout-center {
text-align: center;
}
.phabricator-remarkup-embed-layout-inline {
display: inline;
}
.phabricator-remarkup-embed-float-right {
float: right;
margin: .5em 1em 0;
}
.phabricator-remarkup-embed-layout-link {
padding: 6px 6px 6px 42px;
border-radius: 3px;
margin: 0 0 4px;
display: inline-block;
font-weight: bold;
-webkit-font-smoothing: antialiased;
border: 1px solid {$lightblueborder};
border-radius: 3px;
color: {$blacktext};
min-width: 256px;
position: relative;
line-height: 20px;
overflow: hidden;
min-height: 38px;
}
.phabricator-remarkup-embed-layout-icon {
font-size: 28px;
position: absolute;
top: 10px;
left: 10px;
}
.phabricator-remarkup-embed-layout-info {
color: {$lightgreytext};
font-size: {$smallerfontsize};
font-weight: normal;
margin-left: 8px;
}
.phabricator-remarkup-embed-layout-link:hover {
border-color: {$violet};
cursor: pointer;
}
.device-desktop .phabricator-remarkup-embed-layout-link:hover {
text-decoration: none;
}
.phabricator-remarkup-embed-layout-link:hover
.phabricator-remarkup-embed-layout-icon {
color: {$violet};
}
.phabricator-remarkup-embed-layout-info-block {
display: block;
}
.embed-download-form {
display: inline-block;
padding: 0;
margin: 0;
}
.phabricator-remarkup-embed-layout-link
.phabricator-remarkup-embed-layout-download {
color: {$lightgreytext};
border: none;
background: rgba(0, 0, 0, 0);
box-shadow: none;
outline: 0;
padding: 0;
margin: 0;
text-align: left;
text-shadow: none;
border-radius: 0;
font: inherit;
display: inline;
min-width: 0;
font-weight: normal !important;
}
.phabricator-remarkup-embed-layout-download:hover {
color: {$anchor};
text-decoration: underline;
}
.phabricator-remarkup-embed-float-left {
float: left;
margin: .5em 1em 0;
}
.phabricator-remarkup-image-error {
border: 1px solid {$redborder};
background: {$sh-redbackground};
padding: 8px 12px;
color: {$darkgreytext};
}
.phabricator-remarkup-embed-image {
display: inline-block;
border: 3px solid white;
box-shadow: 1px 1px 2px rgba({$alphablack}, 0.20);
}
.phabricator-remarkup-embed-image-full,
.phabricator-remarkup-embed-image-wide {
display: inline-block;
max-width: 100%;
}
.phabricator-remarkup-embed-image-full img,
.phabricator-remarkup-embed-image-wide img {
height: auto;
max-width: 100%;
}
.phabricator-remarkup .remarkup-table-wrap {
overflow-x: auto;
}
.phabricator-remarkup table.remarkup-table {
border-collapse: separate;
border-spacing: 1px;
background: {$lightblueborder};
margin: 12px 0;
word-break: normal;
}
.phabricator-remarkup table.remarkup-table th {
font-weight: bold;
padding: 4px 6px;
background: {$lightbluebackground};
}
.phabricator-remarkup table.remarkup-table td {
background: {$page.content};
padding: 3px 6px;
}
body div.phabricator-remarkup.remarkup-has-toc
.phabricator-remarkup-toc + .remarkup-header {
margin-top: 0;
padding-top: 0;
}
body .phabricator-standard-page div.phabricator-remarkup *:first-child,
body .phabricator-standard-page div.phabricator-remarkup .remarkup-header + * {
margin-top: 0;
}
body div.phabricator-remarkup > *:last-child {
margin-bottom: 0;
}
.remarkup-assist-textarea {
border-left-color: {$greyborder};
border-right-color: {$greyborder};
border-bottom-color: {$greyborder};
border-top-color: {$thinblueborder};
border-radius: 0;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
box-shadow: none;
-webkit-box-shadow: none;
/* Set line height explicitly so the metrics <var /> and the real textarea
are forced to the same value. */
line-height: 1.25em;
/* Prevent Safari and Chrome users from dragging the textarea any wider,
because the top bar won't resize along with it. */
resize: vertical;
}
var.remarkup-assist-textarea {
/* This is an invisible element used to measure the size of text in the
textarea so we can float typeaheads over the cursor position. */
display: block;
border-color: orange;
box-sizing: border-box;
padding: 4px 6px;
white-space: pre-wrap;
visibility: hidden;
}
.remarkup-assist-textarea:focus {
border: 1px solid rgba(82, 168, 236, 0.8);
}
.remarkup-assist-bar {
height: 32px;
border-width: 1px 1px 0;
border-style: solid;
border-top-color: {$greyborder};
border-left-color: {$greyborder};
border-right-color: {$greyborder};
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 0 4px;
background: {$lightbluebackground};
overflow: hidden;
}
.remarkup-assist-button {
display: block;
margin-top: 4px;
height: 20px;
padding: 2px 5px 3px;
line-height: 18px;
width: 16px;
float: left;
border-radius: 3px;
}
.remarkup-assist-button:hover .phui-icon-view.phui-font-fa {
color: {$sky};
}
.remarkup-assist-button:active {
outline: none;
}
.remarkup-assist-button:focus {
outline: none;
}
.remarkup-assist-separator {
display: block;
float: left;
height: 18px;
margin: 7px 6px;
width: 0px;
border-right: 1px solid {$lightgreyborder};
}
.remarkup-interpreter-error {
padding: 8px;
border: 1px solid {$sh-redborder};
background-color: {$sh-redbackground};
}
.remarkup-cowsay {
white-space: pre-wrap;
}
.remarkup-figlet {
white-space: pre-wrap;
}
.remarkup-assist {
width: 14px;
height: 14px;
overflow: hidden;
text-align: center;
vertical-align: middle;
}
.remarkup-assist-right {
float: right;
}
.jx-order-mask {
background: white;
opacity: 1.0;
}
.phabricator-image-macro-hero {
margin: auto;
max-width: 95%;
}
.phabricator-remarkup-macro {
height: auto;
max-width: 100%;
}
.remarkup-nav-sequence-arrow {
color: {$lightgreytext};
}
.phabricator-remarkup hr {
background: {$thinblueborder};
margin: 24px 0;
clear: both;
}
.phabricator-remarkup .remarkup-highlight {
background-color: {$lightviolet};
padding: 0 4px;
}
.device .remarkup-assist-nodevice {
display: none;
}
/* - Autocomplete ----------------------------------------------------------- */
.phuix-autocomplete {
position: absolute;
width: 300px;
box-shadow: {$dropshadow};
background: {$page.content};
border: 1px solid {$lightgreyborder};
border-radius: 3px;
}
.phuix-autocomplete-head {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 6px 8px;
background: {$lightgreybackground};
color: {$darkgreytext};
border-radius: 3px;
}
.phuix-autocomplete-head .phui-icon-view {
margin-right: 4px;
color: {$lightgreytext};
}
.phuix-autocomplete-echo {
margin-left: 4px;
color: {$lightgreytext};
}
.phuix-autocomplete-list a.jx-result {
display: block;
padding: 5px 8px;
font-size: {$normalfontsize};
border-top: 1px solid {$thinblueborder};
font-weight: bold;
color: {$darkgreytext};
}
.phuix-autocomplete-list a.jx-result .tokenizer-result-closed {
color: {$lightgreytext};
text-decoration: line-through;
}
.phuix-autocomplete-list a.jx-result .phui-icon-view {
margin-right: 4px;
color: {$lightbluetext};
}
.phuix-autocomplete-list a.jx-result:hover {
text-decoration: none;
background: {$sh-bluebackground};
color: {$blacktext};
}
.phuix-autocomplete-list a.jx-result.focused,
.phuix-autocomplete-list a.jx-result.focused:hover {
background: {$sh-bluebackground};
color: {$blacktext};
}
/* - Pinned ----------------------------------------------------------------- */
.phui-box.phui-object-box.phui-comment-form-view.remarkup-assist-pinned {
position: fixed;
background-color: {$page.content};
border-top: 1px solid {$lightblueborder};
box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
width: 100%;
bottom: 0;
left: 0;
right: 0;
margin: 0;
overflow: auto;
- max-height: 40vh;
+ max-height: 60vh;
}
.remarkup-assist-pinned-spacer {
position: relative;
}
/* - c4science customization ---------------------------------------------------------------- */
.phabricator-remarkup .remarkup-code-block pre {
white-space: pre;
}
.phui-feed-story-body .remarkup-code-block pre,
.phabricator-inline-summary-table .phabricator-remarkup .remarkup-code-block pre {
white-space: pre-wrap;
}
/* - Preview ---------------------------------------------------------------- */
.remarkup-inline-preview {
display: block;
position: relative;
background: {$page.content};
overflow-y: auto;
box-sizing: border-box;
width: 100%;
resize: vertical;
padding: 8px;
border: 1px solid {$lightblueborder};
border-top: none;
-webkit-font-smoothing: antialiased;
}
.remarkup-control-fullscreen-mode .remarkup-inline-preview {
resize: none;
}
.remarkup-inline-preview * {
resize: none;
}
.remarkup-assist-button.preview-active {
background: {$sky};
}
.remarkup-assist-button.preview-active .phui-icon-view {
color: {$page.content};
}
.remarkup-assist-button.preview-active:hover {
text-decoration: none;
}
.remarkup-assist-button.preview-active:hover .phui-icon-view {
color: {$page.content};
}
.remarkup-preview-active .remarkup-assist,
.remarkup-preview-active .remarkup-assist-separator {
opacity: .2;
transition: all 100ms cubic-bezier(0.250, 0.250, 0.750, 0.750);
transition-timing-function: cubic-bezier(0.250, 0.250, 0.750, 0.750);
}
.remarkup-preview-active .remarkup-assist-button {
pointer-events: none;
cursor: default;
}
.remarkup-preview-active .remarkup-assist-button.preview-active {
pointer-events: inherit;
cursor: pointer;
}
.remarkup-preview-active .remarkup-assist.fa-eye {
opacity: 1;
transition: all 100ms cubic-bezier(0.250, 0.250, 0.750, 0.750);
transition-timing-function: cubic-bezier(0.250, 0.250, 0.750, 0.750);
}
/* - Fullscreen ------------------------------------------------------------- */
.remarkup-fullscreen-mode {
overflow: hidden;
}
.remarkup-control-fullscreen-mode {
position: fixed;
border: none;
top: 32px;
bottom: 32px;
left: 64px;
right: 64px;
border-radius: 3px;
box-shadow: 0px 4px 32px #555;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button {
padding: 1px 6px 4px;
font-size: 15px;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button .remarkup-assist {
height: 16px;
width: 16px;
}
.aphront-form-input .remarkup-control-fullscreen-mode .remarkup-assist-bar {
border: none;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
height: 32px;
padding: 4px 8px;
background: {$bluebackground};
}
.aphront-form-control .remarkup-control-fullscreen-mode
textarea.remarkup-assist-textarea {
position: absolute;
top: 39px;
left: 0;
right: 0;
height: calc(100% - 36px) !important;
padding: 16px;
font-size: {$biggerfontsize};
line-height: 1.51em;
border-width: 1px 0 0 0;
outline: none;
resize: none;
background: {$page.content} !important;
}
.remarkup-control-fullscreen-mode textarea.remarkup-assist-textarea:focus {
border-color: {$thinblueborder};
box-shadow: none;
}
.remarkup-control-fullscreen-mode .remarkup-inline-preview {
font-size: {$biggerfontsize};
border: none;
padding: 16px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button .fa-arrows-alt {
color: {$sky};
}
.device-phone .remarkup-control-fullscreen-mode {
top: 0;
bottom: 0;
left: 0;
right: 0;
}
diff --git a/webroot/rsrc/css/layout/phabricator-source-code-view.css b/webroot/rsrc/css/layout/phabricator-source-code-view.css
index 6f2864d06..9b61425d6 100644
--- a/webroot/rsrc/css/layout/phabricator-source-code-view.css
+++ b/webroot/rsrc/css/layout/phabricator-source-code-view.css
@@ -1,125 +1,134 @@
/**
* @provides phabricator-source-code-view-css
*/
.phabricator-source-code-container {
overflow-x: auto;
overflow-y: hidden;
border: 1px solid {$paste.border};
- border-radius: 3px;
}
.phui-oi .phabricator-source-code-container {
margin-left: 8px;
}
.phabricator-source-code {
white-space: pre-wrap;
padding: 2px 8px 1px;
width: 100%;
background: #ffffff;
}
.phabricator-source-line {
background-color: {$paste.highlight};
text-align: right;
border-right: 1px solid {$paste.border};
color: {$sh-yellowtext};
+ white-space: nowrap;
}
.phabricator-source-line > a::before {
/* Render the line numbers as a pseudo-element so they don't get copied. */
content: attr(data-n);
}
th.phabricator-source-line a,
th.phabricator-source-line span {
display: block;
padding: 2px 6px 1px 12px;
}
th.phabricator-source-line a {
color: {$darkbluetext};
}
th.phabricator-source-line a:hover {
background: {$paste.border};
text-decoration: none;
}
+.phabricator-source-coverage-highlight .phabricator-source-code,
.phabricator-source-highlight .phabricator-source-code {
background: {$paste.highlight};
}
+.phabricator-source-coverage-highlight .phabricator-source-line,
.phabricator-source-highlight .phabricator-source-line {
background: {$paste.border};
}
.phabricator-source-code-summary {
padding-bottom: 8px;
}
/* If a Paste has enormously long lines, truncate them in the summary on the
list page. They'll be fully visible on the Paste itself. */
.phabricator-source-code-summary .phabricator-source-code-container {
overflow-x: hidden;
}
.phabricator-source-code-summary .phabricator-source-code {
white-space: nowrap;
}
.phabricator-source-blame-skip,
.phabricator-source-blame-info {
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.phabricator-source-blame-skip {
min-width: 28px;
border-right: 1px solid {$thinblueborder};
}
.phabricator-source-blame-info {
white-space: nowrap;
min-width: 130px;
border-right: 1px solid {$paste.border};
padding-right: 8px;
vertical-align: middle;
color: #ffffff;
}
.phabricator-source-blame-info a {
color: {$darkbluetext};
- text-shadow: 1px 1px rgba(0, 0, 0, 0.111);
+ text-shadow: 1px 1px rgba(0, 0, 0, 0.05);
}
.phabricator-source-blame-skip a {
/* Give the user a larger click target. */
display: block;
padding: 2px 8px;
}
.phabricator-source-blame-skip a .phui-icon-view {
color: {$darkbluetext};
}
.device-desktop .phabricator-source-blame-skip a:hover {
background: {$bluebackground};
}
.phabricator-source-blame-author {
display: inline-block;
vertical-align: middle;
padding: 0;
margin: 0 6px 0 8px;
width: 16px;
height: 16px;
background-size: 100% 100%;
background-repeat: no-repeat;
}
+
+th.phabricator-source-coverage {
+ padding: 0 8px;
+ border-left: 1px solid {$thinblueborder};
+ background: {$lightgreybackground};
+ cursor: w-resize;
+}
diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
index 9436d171d..6f2421ca2 100644
--- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
+++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
@@ -1,718 +1,722 @@
/**
* @provides phui-oi-list-view-css
*/
.phui-oi {
border-left-width: 0;
}
ul.phui-oi-list-view {
padding: 8px;
list-style: none;
}
.device-desktop .phui-oi-list-view {
padding: 16px;
}
.phui-oi-list-view + .phui-oi-list-view {
padding-top: 0;
}
.phui-object-box .phui-oi-list-view .phui-oi {
margin: 0;
}
.phui-oi-list-view .phui-info-view {
margin: 0;
}
.phui-object-box .phui-oi-list-view .phui-info-view {
color: {$greytext};
border: none;
}
.phui-oi {
border-style: solid;
border-color: {$lightgreyborder};
margin: 5px 0;
overflow: hidden;
background: {$page.content};
margin-bottom: 4px;
}
.phui-oi .phui-icon-view {
display: inline-block;
}
.phui-oi-frame {
border-color: {$lightblueborder};
border-width: 1px 1px 1px 0;
border-style: solid;
position: relative;
min-height: 33px;
overflow: hidden;
}
.phui-oi-cover-image {
display: none;
}
.phui-oi-no-bar .phui-oi-frame {
border-width: 1px;
}
.device-desktop .phui-oi {
margin: 0 0 4px 0;
}
.phui-object-box .phui-oi-list-view {
margin: 0;
}
.phui-oi-status-icon {
font-weight: bold;
padding: 3px;
font-size: 16px;
}
.phui-oi-list-view .phui-oi-col0 .phui-icon-view {
width: 17px;
text-align: center;
overflow: visible;
position: relative;
left: -1px;
}
.phui-oi-name {
padding: 8px 8px 0;
white-space: nowrap;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
font-weight: bold;
-webkit-font-smoothing: antialiased;
}
.device-phone .phui-oi-name {
overflow: normal;
white-space: normal;
font-weight: bold;
}
.phui-oi-link {
display: inline;
}
.phui-oi-objname {
color: {$blacktext};
cursor: text;
font-weight: bold;
}
.phui-oi-content {
margin: 4px 8px 2px 0;
overflow: hidden;
}
.phui-oi-grippable {
cursor: move;
}
.device .phui-oi-grippable {
cursor: normal;
}
.phui-oi-grip {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 20px;
background: url('/rsrc/image/texture/grip.png') center center no-repeat;
}
.device .phui-oi-grip {
display: none;
}
.phui-oi-grippable .phui-oi-frame {
padding-left: 16px;
}
.device .phui-oi-grippable .phui-oi-frame {
padding-left: 0;
}
.phui-oi-list-header {
padding: 0 0 8px 0;
color: {$darkgreytext};
}
.phui-oi-table {
display: table;
table-layout: fixed;
width: 100%;
}
.phui-oi-table-row {
display: table-row;
}
.phui-oi-col0 {
width: 20px;
display: table-cell;
vertical-align: middle;
padding-left: 4px;
}
.device-phone .phui-oi-col0 {
vertical-align: top;
padding-top: 8px;
}
.phui-oi-col1 {
display: table-cell;
vertical-align: top;
}
.phui-oi-col2 {
width: 160px;
display: table-cell;
vertical-align: top;
}
.phui-oi-col2.phui-oi-side-column {
width: 200px;
}
.device-phone .phui-oi-col1,
.device-phone .phui-oi-col2 {
display: block;
width: auto;
}
/* - Item Actions --------------------------------------------------------------
Action buttons, like "Edit" and "Delete".
*/
.phui-oi-actions {
position: absolute;
right: 4px;
top: 4px;
bottom: 4px;
vertical-align: middle;
text-align: right;
}
.phui-oi-actions .phui-list-item-view {
float: right;
height: 100%;
width: 24px;
display: inline-block;
position: relative;
}
.phui-oi-actions .phui-list-item-href {
display: inline-block;
position: relative;
width: 24px;
height: 100%;
}
.device-desktop .phui-oi-actions .phui-list-item-href:hover {
background: {$hoverblue};
border-radius: 3px;
}
.phui-oi-actions .phui-list-item-icon {
width: 14px;
height: 14px;
position: absolute;
display: block;
top: 50%;
margin-top: -7px;
left: 3px;
}
.phui-oi-actions .phui-list-item-name {
display: none;
}
.phui-oi-with-1-actions .phui-oi-content-box {
margin-right: 28px;
overflow: hidden;
}
.phui-oi-with-2-actions .phui-oi-content-box {
margin-right: 54px;
overflow: hidden;
}
.phui-oi-with-3-actions .phui-oi-content-box {
margin-right: 76px;
overflow: hidden;
}
/* - Object Box List -----------------------------------------------------------
Tighter, stacking list when inside an Object Box
*/
.phui-object-box .phui-oi-list-view {
padding: 0;
border: none;
}
.phui-object-box .phui-oi-frame {
border-right: none;
}
.phui-object-box .phui-oi:last-child
.phui-oi-frame {
border-bottom: none;
}
/* - Subhead -------------------------------------------------------------------
Descriptive Text or Links under the main header, before attributes.
*/
.phui-oi-subhead {
color: {$greytext};
padding: 0 8px 6px;
}
.phui-oi-description {
display: none;
}
.phui-oi-description.phui-oi-description-reveal {
display: block;
}
.phui-oi-description-tag {
margin-left: 4px;
}
.phui-oi-description-tag:hover .phui-tag-core {
cursor: pointer;
background: {$darkgreybackground};
}
.phui-oi-description-tag .phui-tag-core {
border: none;
}
.phui-oi-description-tag.phui-tag-view .phui-icon-view {
margin: 2px;
}
/* - Attribute List ------------------------------------------------------------
Object attributes, commonly used to render created date, etc.
*/
.phui-oi-attributes {
padding: 0 8px 6px;
line-height: 18px;
min-height: 21px;
}
.phui-oi-attribute {
display: inline-block;
color: {$greytext};
vertical-align: top;
margin-right: 4px;
}
.phui-oi-attribute-spacer {
padding: 0 4px;
}
/* - Icons ---------------------------------------------------------------------
Icons, which show object state. On mobile, they are rendered without labels
to save space.
*/
.phui-object-icon-pane {
margin: 8px 0 4px;
}
.device-phone .phui-object-icon-pane {
margin: 0 0 4px;
}
.phui-oi-icons {
padding: 0 4px 0 0;
}
.device-phone .phui-oi-icons {
padding: 0 0 0 8px;
}
ul.phui-oi-icons {
margin: 0;
}
.phui-oi-icon {
vertical-align: middle;
font-size: {$smallerfontsize};
color: {$greytext};
text-align: right;
white-space: nowrap;
overflow: hidden;
min-height: 18px;
line-height: 18px;
}
.device-phone .phui-oi-icon {
text-align: left;
font-size: 13px;
}
/*
* Items with icon 'none' still have on mobile, thus creating a weird vertical
* margin for elements which follow
*/
.device-phone .phui-oi-icon .none {
display: none;
}
.phui-oi-icon-image {
width: 14px;
height: 14px;
font-size: 13px;
margin-right: 4px;
}
/* - Disabled ------------------------------------------------------------------
Disabled/inactive objects.
*/
.phui-oi.phui-oi-disabled .phui-oi-link,
.phui-oi.phui-oi-disabled .phui-oi-link a {
color: {$lightgreytext};
}
.phui-oi.phui-oi-disabled .phui-oi-frame {
border-color: #d7d7d7;
}
.phui-oi.phui-oi-disabled .phui-oi-objname {
color: {$greytext};
text-decoration: line-through;
}
.phui-oi.phui-oi-disabled .phui-oi-image {
opacity: .8;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.phui-oi.phui-oi-disabled .phui-oi-attribute,
.phui-oi.phui-oi-disabled .phui-oi-attribute > .phui-icon-view {
color: {$lightgreytext};
}
/* - Effects -------------------------------------------------------------------
Effects like highlighted items.
*/
.phui-oi.phui-oi-highlighted {
background: {$sh-yellowbackground};
}
ul.phui-oi-list-view .phui-oi-highlighted
.phui-oi-frame {
border-color: {$sh-yellowborder};
}
.phui-oi-selected {
background: {$sh-bluebackground};
}
ul.phui-oi-list-view .phui-oi-selected
.phui-oi-frame {
border-color: {$sh-blueborder};
}
.phui-oi-forbidden {
background: {$sh-redbackground};
}
/* - Handle Icons --------------------------------------------------------------
Shows owners, reviewers, etc., using profile picture icons.
*/
.phui-oi-handle-icons {
bottom: 0;
right: 4px;
position: absolute;
}
.phui-oi-handle-icon {
width: 24px;
height: 24px;
display: inline-block;
background-size: 100%;
border-radius: 3px;
background-repeat: no-repeat;
}
/* - Bylines -------------------------------------------------------------------
Shows owners, authors, reviewers, etc., in text.
*/
.phui-oi-bylines {
padding: 0 4px 0 8px;
margin: 4px 0 8px;
font-size: {$smallerfontsize};
color: {$greytext};
text-align: right;
}
.phui-oi-byline {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.device-phone .phui-oi-bylines {
float: none;
text-align: left;
padding: 0 8px;
font-size: {$normalfontsize};
}
/* - Draggable List ------------------------------------------------------------
These classes are applied by and/or provided for use with JX.DraggableList.
*/
.drag-ghost {
position: relative;
background: {$sh-indigobackground};
border-radius: 3px;
margin-bottom: 4px;
border: 1px dashed {$sh-indigoborder};
}
.drag-dragging {
opacity: 0.25;
}
.drag-sending {
opacity: 0.5;
}
.drag-clone,
.drag-frame {
/* This allows mousewheel events to pass through the clone and frame while
they are being dragged. Without this, the mousewheel does not work during
a drag operation. */
pointer-events: none;
}
.drag-frame {
position: fixed;
overflow: hidden;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.drag-clone {
position: absolute;
list-style: none;
}
/* - Badges ---------------------------------------------------------------- */
.phui-oi-col0.phui-oi-badge {
width: 28px;
}
.phui-oi-col0.phui-oi-badge .phui-icon-view {
left: 0;
}
/* - Countdowns ------------------------------------------------------------ */
.phui-oi-col0.phui-oi-countdown {
width: 52px;
padding: 0;
}
.phui-oi-countdown .phui-oi-countdown-number {
border-right: 1px solid {$thinblueborder};
text-align: center;
color: {$bluetext};
}
/* - Dashboards ------------------------------------------------------------ */
.phui-object-box .phui-oi-list-view .phui-oi-frame {
border: none;
border-bottom: 1px solid {$thinblueborder};
}
.drag-clone.phui-oi-standard .phui-oi-frame {
border: none;
opacity: 0.8;
background: {$sh-bluebackground};
}
.phui-object-box .phui-oi-list-header {
font-size: {$normalfontsize};
color: {$darkbluetext};
border-top: 1px solid {$thinblueborder};
border-bottom: 1px solid {$thinblueborder};
padding: 8px 12px;
background-color: {$lightgreybackground};
}
.phui-object-box .phui-header-shell + .phui-oi-list-view .phui-oi-list-header,
.phui-object-box .phui-object-box-hidden-content + .phui-oi-list-view
.phui-oi-list-header,
.phui-object-box .phui-object-box-hidden-content + .phui-oi-list-header {
border-top: none;
}
.dashboard-pane .phui-oi-empty .phui-info-view {
border: none;
margin: 0;
}
.device-desktop .aphront-multi-column-fluid .aphront-multi-column-2-up
.aphront-multi-column-column-outer.third .phui-oi-col2 {
display: none;
}
/* - Launcher List ---------------------------------------------------------- */
.phui-oi-image-icon {
background: none;
width: 40px;
height: 40px;
margin: 8px 6px;
position: absolute;
}
.phui-oi-image-icon .phui-icon-view {
position: absolute;
width: 40px;
height: 40px;
font-size: 26px;
text-align: center;
line-height: 36px;
}
.phui-oi-image {
width: 40px;
height: 40px;
border-radius: 3px;
background-size: 100%;
margin: 8px 6px;
position: absolute;
}
.phui-oi-with-image-icon .phui-oi-frame,
.phui-oi-with-image .phui-oi-frame {
min-height: 52px;
}
.phui-oi-with-image-icon .phui-oi-content-box,
.phui-oi-with-image .phui-oi-content-box {
margin-left: 46px;
}
/* - Launcher Button -------------------------------------------------------- */
.phui-oi-col2.phui-oi-side-column {
text-align: right;
vertical-align: middle;
padding-right: 4px;
}
.device-phone .phui-oi-col2.phui-oi-side-column {
padding: 0 8px 8px;
text-align: left;
}
.phui-oi-col0.phui-oi-checkbox {
width: 28px;
text-align: center;
}
.phui-oi-selectable {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
/* When the list selection state can be toggled on the client (as in the bulk
editor), keep the border color consistent to make the interaction feel more
robust. */
ul.phui-oi-list-view .phui-oi-selectable
.phui-oi-frame {
border-color: {$blueborder};
}
.differential-revision-size {
padding: 0 4px;
border-radius: 4px;
background: {$lightgreybackground};
cursor: pointer;
}
.differential-revision-size .phui-icon-view {
margin: 0 1px 0 1px;
- font-size: smaller;
- color: {$blueborder};
+ font-size: 7px;
+ position: relative;
+ top: -2px;
+ color: {$lightbluetext};
}
.differential-revision-large {
- background: {$sh-redbackground};
+ background: {$sh-orangebackground};
}
+/* NOTE: These are intentionally using nonstandard colors, see T13127. */
+
.differential-revision-large .phui-icon-view {
- color: {$red};
+ color: #e5ae7e;
}
.differential-revision-small {
- background: {$sh-greenbackground};
+ background: #f2f7ff;
}
.differential-revision-small .phui-icon-view {
- color: {$green};
+ color: #6699ba;
}
diff --git a/webroot/rsrc/css/phui/phui-crumbs-view.css b/webroot/rsrc/css/phui/phui-crumbs-view.css
index 6155e9b2b..0811b3613 100644
--- a/webroot/rsrc/css/phui/phui-crumbs-view.css
+++ b/webroot/rsrc/css/phui/phui-crumbs-view.css
@@ -1,136 +1,138 @@
/**
* @provides phui-crumbs-view-css
*/
.phui-crumbs-view {
overflow: hidden;
vertical-align: top;
padding: 0 12px 0 20px;
/* TODO: Position this over the slider for Differential's file tree view.
Remove this once that gets sorted out. */
position: relative;
-webkit-font-smoothing: antialiased;
background-color: {$page.background};
}
.printable .phui-crumbs-view {
display: none;
}
.phui-crumbs-view,
.phui-crumbs-view a.phui-crumb-view,
.phui-crumbs-view a.phui-crumbs-action {
color: {$darkbluetext};
font-weight: bold;
text-decoration: none;
}
.device-tablet .phui-crumbs-view,
.device-phone .phui-crumbs-view,
.project-board-nav .phui-crumbs-view {
padding-left: 8px;
padding-right: 8px;
}
.phui-crumbs-view a.phui-crumbs-action-disabled,
.phui-crumbs-view a.phui-crumbs-action-disabled .phui-icon-view {
color: {$lightgreytext};
}
.phui-crumbs-view + .phui-header-shell {
border-top: none;
}
.device-desktop .phui-crumbs-view a:hover {
text-decoration: underline;
}
.phui-crumb-view {
float: left;
padding: 8px 0;
max-width: 240px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.device-phone .phui-crumb-view.phabricator-last-crumb .phui-crumb-name,
+.device-phone .phui-crumb-view.phui-crumb-always-visible .phui-crumb-name,
+.device-phone .phui-crumb-view.phui-crumb-always-visible + .phui-crumb-divider,
.device-phone .phui-crumb-view.phui-crumb-has-icon,
.device-phone .phui-crumb-has-icon + .phui-crumb-divider {
display: inline-block;
}
.device-phone .phui-crumb-name,
.device-phone .phui-crumb-divider {
display: none;
}
.phui-crumb-has-icon .phui-icon-view {
margin: 0 4px;
}
.device-phone .phui-crumb-icon {
margin-left: 7px;
}
.phui-crumbs-actions {
float: right;
white-space: nowrap;
}
.phui-crumbs-action {
display: inline-block;
height: 17px;
padding: 8px 12px;
position: relative;
}
.device-phone a.phui-crumbs-action {
padding: 8px 6px;
}
.device-desktop .phui-crumbs-view a:hover,
.device-desktop .phui-crumbs-view a:hover .phui-icon-view {
color: {$blue};
text-decoration: none;
}
.device-phone .phui-crumbs-action-name {
display: none;
}
a.phui-crumbs-action .phui-icon-view {
color: {$darkbluetext};
}
a.phui-crumbs-action .phui-crumbs-action-name {
margin-left: 6px;
}
.device-phone a.phui-crumbs-action .phui-icon-view {
margin-left: 4px;
}
.phui-crumb-divider {
margin: 2px 8px;
}
.phui-crumbs-view.phui-crumbs-border {
border-bottom: 1px solid rgba({$alphagrey},.1);
}
body .phui-crumbs-view + .phui-object-box {
margin-top: 0;
}
body .phui-crumbs-view + .phui-oi-list-view {
padding-top: 0;
}
.phui-crumb-action-divider {
border-left: 1px solid {$lightgreyborder};
}
.phui-crumbs-action-icon + .phui-crumbs-action-icon {
padding-left: 4px;
}
diff --git a/webroot/rsrc/css/phui/phui-document-pro.css b/webroot/rsrc/css/phui/phui-document-pro.css
index 35c843f81..4d58d17cb 100644
--- a/webroot/rsrc/css/phui/phui-document-pro.css
+++ b/webroot/rsrc/css/phui/phui-document-pro.css
@@ -1,257 +1,296 @@
/**
* @provides phui-document-view-pro-css
*/
.phui-document-view.phui-document-view-pro {
max-width: 800px;
- padding: 16px 16px 64px 16px;
+ padding: 16px 16px 32px 16px;
margin: 0 auto;
}
+.phui-document-view.phui-document-view-pro.has-curtain {
+ max-width: 1132px;
+}
+
+.printable .phui-document-view.phui-document-view-pro.has-curtain {
+ max-width: none;
+}
+
+.device-desktop .phui-document-inner {
+ overflow: hidden;
+}
+
+.device-desktop .has-curtain .phui-document-content-view {
+ display: table-cell;
+}
+
+.printable .phui-document-content-view {
+ padding-right: 0;
+}
+
+.device-desktop .phui-document-content-outer {
+ display: table;
+ width: 100%;
+ layout: fixed;
+}
+
+.device-desktop .phui-document-content-inner {
+ display: table-row;
+}
+
+.device-desktop .phui-document-curtain {
+ display: table-cell;
+ width: 300px;
+}
+
+.printable .phui-document-curtain {
+ display: none;
+}
+
.phui-document-container {
background-color: {$page.content};
position: relative;
border-bottom: 1px solid #dedee1;
}
.phui-document-view-pro-box,
.phui-document-properties {
max-width: 800px;
margin: 0 auto;
}
body.printable {
background-color: #fff;
}
.printable .phui-document-view-pro-box {
display: none;
}
.printable .phui-document-container {
border: none;
}
.printable .phui-document-container .phui-header-view .phui-header-subheader {
display: none;
}
.printable .phui-document-container .phui-header-view .phui-header-col3 {
display: none;
}
.device .phui-document-view-pro-box {
margin: 0 8px;
}
.phui-document-view-pro-box .phui-property-list-section {
margin: 16px auto;
}
.device .phui-document-view-pro-box .phui-property-list-section {
margin: 0 8px 16px;
}
.device .phui-document-view-pro-box .phui-property-list-container {
padding: 24px 0 0 0;
}
.device-phone .phui-document-view.phui-document-view-pro {
padding: 0 12px;
margin: 0 auto;
}
.phui-document-view-pro .phui-document-toc {
position: absolute;
top: 34px;
left: -44px;
}
.printable .phui-document-view-pro a.phui-document-toc {
display: none;
}
.phui-document-view-pro .phui-document-toc-list {
margin: 8px;
border: 1px solid {$lightgreyborder};
border-radius: 3px;
box-shadow: {$dropshadow};
width: 260px;
position: absolute;
z-index: 30;
background-color: {$page.content};
top: 52px;
left: -40px;
}
.device .phui-document-view-pro .phui-document-toc {
display: none;
}
.phui-document-toc-list {
display: none;
}
.phui-document-toc-open .phui-document-toc-list {
display: block;
}
.phui-document-toc-open .phui-document-toc {
border-color: {$blueborder};
}
.phui-document-view-pro .phui-document-toc-content {
margin: 8px 16px;
}
.phui-document-view-pro .phui-document-toc-header {
font-weight: bold;
color: {$bluetext};
margin-bottom: 8px;
text-transform: uppercase;
font-size: {$smallerfontsize};
}
.phui-document-view-pro .phui-document-toc-content li {
margin: 4px 8px 4px 0;
}
.phui-document-view-pro .phui-document-toc-content a {
padding: 2px 0;
display: block;
text-decoration: none;
color: {$darkbluetext};
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.phui-document-view-pro .phui-document-toc-content a:hover {
color: {$anchor};
text-decoration: underline;
}
.phui-document-view-pro .phui-document-toc-content li + ul {
margin: 4px 0 4px 8px;
}
.phui-document-view-pro .phui-document-content .phabricator-remarkup {
padding: 16px 0;
line-height: 1.7em;
}
.device-desktop .phui-document-view.phui-document-view-pro {
border: 0;
}
.phui-document-view.phui-document-view-pro .phui-header-shell {
background: transparent;
border-bottom: 1px solid {$thinblueborder};
}
.phui-document-view.phui-document-view-pro .phui-header-shell {
margin: 0;
padding: 16px 0 32px;
}
.device-phone .phui-document-view.phui-document-view-pro .phui-header-shell {
margin: 0;
padding: 16px 0 20px;
}
.phui-document-view.phui-document-view-pro .phui-header-tall
.phui-header-header {
font-size: 24px;
line-height: 30px;
color: {$blacktext};
}
.device-phone .phui-document-view.phui-document-view-pro .phui-header-tall
.phui-header-header {
font-size: 18px;
}
.device-phone .phui-document-view-pro .phui-header-subheader {
display: block;
padding: 8px 0 0 0;
}
.phui-document-view-pro .phui-info-view {
margin: 16px 0;
}
.phui-document-view-pro .phabricator-remarkup-embed-image-wide {
margin-left: -200px;
margin-right: -200px;
width: auto;
}
.phui-document-view-pro .phabricator-remarkup-embed-image-wide img {
max-width: 1200px;
}
@media (max-width: 1200px) {
.phui-document-view-pro .phabricator-remarkup-embed-image-wide {
margin-left: 0;
margin-right: 0;
width: auto;
}
.phui-document-view-pro .phabricator-remarkup-embed-image-wide img {
max-width: inherit;
}
}
.phui-document-view-pro-box .phui-timeline-view {
padding: 16px 0 0 0;
background: none;
border-top: 1px solid rgba({$alphablue}, 0.20);
}
.phui-document-view-pro-box .phui-timeline-wedge {
display: none;
}
.phui-document-view-pro-box .phui-timeline-major-event .phui-timeline-group {
border: none;
}
.phui-document-view-pro-box .phui-timeline-major-event .phui-timeline-content {
border: none;
}
.device-desktop .phui-document-view-pro-box
.phui-timeline-event-view.phui-timeline-minor-event {
margin-left: 62px;
}
.phui-document-view-pro-box .phui-timeline-title {
border-top-right-radius: 3px;
border-top-left-radius: 3px;
background-color: {$page.content};
border-bottom: 1px solid #F1F1F4;
}
.phui-document-view-pro-box .phui-timeline-title-with-icon {
padding-left: 12px;
}
.phui-document-view-pro-box .phui-timeline-icon-fill {
display: none;
}
.phui-document-view-pro-box .phui-object-box {
margin: 0;
}
.phui-document-view-pro-box .phui-object-box .remarkup-assist-textarea {
height: 9em;
}
.document-has-foot .phui-document-view-pro {
padding-bottom: 0;
}
.phui-document-foot-content {
margin: 64px 0 32px;
}
diff --git a/webroot/rsrc/css/phui/phui-document.css b/webroot/rsrc/css/phui/phui-document.css
index 74da51dc5..0307f94c8 100644
--- a/webroot/rsrc/css/phui/phui-document.css
+++ b/webroot/rsrc/css/phui/phui-document.css
@@ -1,106 +1,109 @@
/**
* @provides phui-document-view-css
* @requires katex-css
*/
.phui-document-view {
margin-bottom: 16px;
border-radius: 3px;
position: relative;
}
.device-desktop .phui-document-view {
border: 1px solid {$lightblueborder};
max-width: 960px;
margin: 16px auto;
}
.device-desktop .phui-document-box {
max-width: 996px;
margin: 24px auto;
}
.device-desktop .phui-document-fluid .phui-document-view {
max-width: none;
margin: 16px;
}
/* Fix so that Phriction Document preview is the same width as the document */
.device-desktop .phui-remarkup-preview .phui-document-view {
width: 800px;
}
.phui-document-content .phui-header-shell {
border-top: none;
border-bottom: 1px solid {$lightblueborder};
}
.phui-document-content
.phui-header-shell.phui-header-no-background {
border-bottom: 1px solid {$thinblueborder};
margin: 0 0 16px 0;
}
.phui-document-content
.phui-header-shell.phui-header-no-background
.phui-header-view {
padding: 8px 0 4px;
}
.legalpad .phui-document-content .phui-property-list-view {
border: none;
box-shadow: none;
border-radius: 3px;
margin: 16px 0 0 0;
background-color: {$bluebackground};
}
.phui-document-content {
background: {$page.content};
}
.phui-document-content .phabricator-remarkup {
padding: 16px;
font-size: 14px;
}
-.phui-document-view .phui-header-action-links .phui-mobile-menu {
+.phui-document-view.has-no-curtain
+ .phui-header-action-links .phui-mobile-menu {
display: block;
}
.device-phone .phui-document-content .phabricator-remarkup {
padding: 8px;
}
-.device-desktop .phui-document-content .phabricator-action-list-view {
- display: none;
-}
-
.phui-document-content .phabricator-remarkup .remarkup-code-block {
clear: both;
margin: 16px 0;
}
.phui-document-view .phui-info-severity-nodata {
background-color: {$lightgreybackground};
}
.phui-document-view .phui-property-list-section-header {
padding: 20px 24px 0px;
border-top: none;
}
.phui-document-view .phui-property-list-text-content {
padding: 0 24px 4px;
}
.phui-document-view .PhabricatorMonospaced,
.phui-document-view .phabricator-remarkup .remarkup-code-block .remarkup-code {
font: 12px/18px "Menlo", "Consolas", "Monaco", monospace;
}
.platform-windows .phui-document-view .PhabricatorMonospaced,
.platform-windows .phui-document-view .phabricator-remarkup .remarkup-code-block
.remarkup-code {
font: 13px/18px "Menlo", "Consolas", "Monaco", monospace;
}
+
+.phui-document-version-navigation {
+ text-align: center;
+ padding: 8px;
+ background-color: {$lightgreybackground};
+}
diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css
index 1b7400656..982419688 100644
--- a/webroot/rsrc/css/phui/phui-form-view.css
+++ b/webroot/rsrc/css/phui/phui-form-view.css
@@ -1,564 +1,565 @@
/**
* @provides phui-form-view-css
*/
.phui-form-view {
padding: 16px;
}
.device-phone .phui-object-box .phui-form-view {
padding: 0;
}
.phui-form-view.phui-form-full-width {
padding: 0;
}
.phui-form-view label.aphront-form-label {
width: 19%;
height: 28px;
line-height: 28px;
float: left;
text-align: right;
font-weight: bold;
font-size: {$normalfontsize};
color: {$bluetext};
-webkit-font-smoothing: antialiased;
}
.device-phone .phui-form-view label.aphront-form-label,
.phui-form-full-width.phui-form-view label.aphront-form-label {
display: block;
float: none;
text-align: left;
width: 100%;
margin-bottom: 3px;
}
.aphront-form-input {
margin-left: 20%;
margin-right: 20%;
width: 60%;
}
.device-phone .aphront-form-input,
.device .aphront-form-input select,
.device .aphront-form-input pre,
.phui-form-full-width .aphront-form-input {
margin-left: 0%;
margin-right: 0%;
width: 100%;
}
.aphront-form-input *::-webkit-input-placeholder {
color:{$greytext} !important;
}
.aphront-form-input *::-moz-placeholder {
color:{$greytext} !important;
opacity: 1; /* Firefox nudges the opacity to 0.4 */
}
.aphront-form-input *:-ms-input-placeholder {
color:{$greytext} !important;
}
.aphront-form-error {
width: 18%;
float: right;
color: {$red};
font-weight: bold;
padding-top: 5px;
}
.aphront-form-label .aphront-form-error {
display: none;
}
.aphront-dialog-body .phui-form-view {
padding: 0;
}
.device-phone .aphront-form-error,
.phui-form-full-width .aphront-form-error {
display: none;
}
.device-phone .aphront-form-label .aphront-form-error,
.phui-form-full-width .aphront-form-label .aphront-form-error {
display: block;
float: right;
padding: 0;
width: auto;
}
.device-phone .aphront-form-drag-and-drop-upload {
display: none;
}
.aphront-form-required {
font-weight: normal;
color: {$lightgreytext};
font-size: {$smallestfontsize};
-webkit-font-smoothing: antialiased;
}
.aphront-form-input input[type="text"],
.aphront-form-input input[type="password"] {
width: 100%;
}
.aphront-form-cvc-input input {
width: 64px;
}
.aphront-form-input textarea {
display: block;
width: 100%;
box-sizing: border-box;
height: 12em;
}
.aphront-form-control {
padding: 4px;
}
.device-phone .aphront-form-control {
padding: 4px 8px 8px;
}
.phui-form-full-width .aphront-form-control {
padding: 4px 0;
}
.aphront-form-control-submit button,
.aphront-form-control-submit a.button {
float: right;
margin: 4px 0 0 8px;
}
.phui-form-control-multi-submit input,
.phui-form-control-multi-submit button,
.phui-form-control-multi-submit a {
float: right;
margin: 4px 0 0 8px;
width: auto;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-short {
height: 44px;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-tall {
height: 24em;
}
.phui-form-view .aphront-form-caption {
font-size: {$smallerfontsize};
color: {$bluetext};
padding: 8px 0;
margin-right: 20%;
margin-left: 20%;
-webkit-font-smoothing: antialiased;
line-height: 16px;
}
.device-phone .phui-form-view .aphront-form-caption,
.phui-form-full-width .phui-form-view .aphront-form-caption {
margin: 0;
}
.aphront-form-instructions {
width: 60%;
margin-left: 20%;
padding: 12px 4px;
color: {$darkbluetext};
}
.device .aphront-form-instructions,
.phui-form-full-width .aphront-form-instructions {
width: auto;
margin: 0;
padding: 12px 8px 8px;
}
.aphront-form-important {
margin: .5em 0;
background: #ffffdd;
padding: .5em 1em;
}
.aphront-form-important code {
display: block;
padding: .25em;
margin: .5em 2em;
}
.aphront-form-control-markup .aphront-form-input {
font-size: {$normalfontsize};
padding: 3px 0;
}
.aphront-form-control-static .aphront-form-input {
line-height: 28px;
}
.aphront-form-control-togglebuttons .aphront-form-input {
padding: 2px 0 0 0;
}
table.aphront-form-control-radio-layout,
table.aphront-form-control-checkbox-layout {
margin-top: 4px !important;
font-size: {$normalfontsize};
}
table.aphront-form-control-radio-layout th {
padding-left: 8px;
padding-bottom: 8px;
font-weight: bold;
color: {$darkgreytext};
}
table.aphront-form-control-checkbox-layout th {
padding-top: 2px;
padding-left: 8px;
padding-bottom: 4px;
color: {$darkgreytext};
}
.aphront-form-control-radio-layout td input,
.aphront-form-control-checkbox-layout td input {
margin-top: 4px;
width: auto;
}
.aphront-form-control-radio-layout label.disabled,
.aphront-form-control-checkbox-layout label.disabled {
color: {$greytext};
}
.aphront-form-radio-caption {
margin-top: 4px;
font-size: {$smallerfontsize};
font-weight: normal;
color: {$bluetext};
}
.aphront-form-control-image span {
margin: 0 4px 0 2px;
}
.aphront-form-control-image .default-image {
display: inline;
width: 12px;
}
.aphront-form-input hr {
border: none;
background: #bbbbbb;
height: 1px;
position: relative;
}
.phui-form-inset {
margin: 12px 0;
padding: 8px;
background: #f7f9fd;
border: 1px solid {$lightblueborder};
border-radius: 3px;
}
.phui-form-inset h1 {
color: {$bluetext};
padding-bottom: 8px;
margin-bottom: 8px;
font-size: {$biggerfontsize};
border-bottom: 1px solid {$thinblueborder};
}
.aphront-form-drag-and-drop-file-list {
width: 400px;
}
.drag-and-drop-instructions {
color: {$darkgreytext};
font-size: {$smallestfontsize};
padding: 6px 8px;
}
.drag-and-drop-file-target {
border: 1px dashed #bfbfbf;
padding-top: 12px;
padding-bottom: 12px;
}
body .phui-form-view .remarkup-assist-textarea.aphront-textarea-drag-and-drop {
background: {$sh-greenbackground};
border: 1px solid {$sh-greenborder};
}
.aphront-form-crop .crop-box {
cursor: move;
overflow: hidden;
}
.aphront-form-crop .crop-box .crop-image {
position: relative;
top: 0px;
left: 0px;
}
.calendar-button {
display: inline;
padding: 8px 4px;
margin: 2px 8px 2px 2px;
position: relative;
}
.aphront-form-date-container {
position: relative;
display: inline;
}
.aphront-form-date-container select {
margin: 2px;
display: inline;
}
.aphront-form-date-container input.aphront-form-date-enabled-input {
width: auto;
display: inline;
margin-right: 8px;
font-size: 16px;
}
.aphront-form-date-container .aphront-form-time-input-container,
.aphront-form-date-container .aphront-form-date-input-container {
position: relative;
display: inline-block;
width: 7em;
}
.aphront-form-date-container input.aphront-form-time-input,
.aphront-form-date-container input.aphront-form-date-input {
width: 7em;
}
.aphront-form-time-input-container div.jx-typeahead-results a.jx-result {
border: none;
}
.phui-time-typeahead-value {
padding: 4px;
}
.fancy-datepicker {
position: absolute;
width: 240px;
}
.device .fancy-datepicker {
width: 100%;
}
.fancy-datepicker-core {
width: 240px;
margin: 0 auto;
padding: 1px;
font-size: {$smallerfontsize};
text-align: center;
}
.fancy-datepicker-core .month-table,
.fancy-datepicker-core .day-table {
margin: 0 auto;
border-collapse: separate;
border-spacing: 1px;
width: 100%;
}
.fancy-datepicker-core .month-table {
margin-bottom: 6px;
font-size: {$normalfontsize};
background-color: {$hoverblue};
border-radius: 2px;
}
.fancy-datepicker-core .month-table td.lrbutton {
width: 18%;
color: {$lightbluetext};
}
.fancy-datepicker-core .month-table td {
padding: 4px;
font-weight: bold;
color: {$bluetext};
}
.fancy-datepicker-core .month-table td.lrbutton:hover {
border-radius: 2px;
background: {$hoverselectedblue};
color: {$darkbluetext};
}
.fancy-datepicker-core .day-table td {
overflow: hidden;
vertical-align: center;
text-align: center;
border: 1px solid {$thinblueborder};
padding: 4px 0;
}
.fancy-datepicker .fancy-datepicker-core .day-table td.day:hover {
background-color: {$hoverblue};
border-color: {$lightblueborder};
}
.fancy-datepicker-core .day-table td.day-placeholder {
border-color: transparent;
background: transparent;
}
.fancy-datepicker-core .day-table td.weekend {
color: {$lightgreytext};
border-color: {$lightgreyborder};
background: {$lightgreybackground};
}
.fancy-datepicker-core .day-table td.day-name {
background: transparent;
border: 1px transparent;
vertical-align: bottom;
color: {$lightgreytext};
}
.fancy-datepicker-core .day-table td.today {
background: {$greybackground};
border-color: {$greyborder};
color: {$darkgreytext};
font-weight: bold;
}
.fancy-datepicker-core .day-table td.datepicker-selected {
background: {$lightgreen};
border-color: {$green};
color: {$green};
}
.fancy-datepicker-core td {
cursor: pointer;
}
.fancy-datepicker-core td.novalue {
cursor: inherit;
}
.picker-open .calendar-button .phui-icon-view {
color: {$sky};
}
.fancy-datepicker-core {
background-color: white;
border: 1px solid {$lightgreyborder};
box-shadow: {$dropshadow};
border-radius: 3px;
}
/* When the activation checkbox for the control is toggled off, visually
disable the individual controls. We don't actually use the "disabled" property
because we still want the values to submit. This is just a visual hint that
the controls won't be used. The controls themselves are still live, work
properly, and submit values. */
.datepicker-disabled select,
.datepicker-disabled .calendar-button,
.datepicker-disabled input[type="text"] {
opacity: 0.5;
}
.aphront-form-date-container.no-time .aphront-form-time-input{
display: none;
}
.login-to-comment {
margin: 12px;
}
.phui-form-divider hr {
height: 1px;
border: 0;
background: {$thinblueborder};
width: 85%;
margin: 15px auto;
}
.recaptcha_only_if_privacy {
display: none;
}
.phabricator-standard-custom-field-header {
font-size: 16px;
color: {$bluetext};
border-bottom: 1px solid {$lightbluetext};
padding: 16px 0 4px;
margin-bottom: 4px;
}
.device-desktop .text-with-submit-control-outer-bounds {
position: relative;
}
.device-desktop .text-with-submit-control-text-bounds {
position: absolute;
left: 0;
right: 184px;
}
.device-desktop .text-with-submit-control-submit-bounds {
text-align: right;
}
.device-desktop .text-with-submit-control-submit {
width: 180px;
}
.phui-form-iconset-table td {
vertical-align: middle;
padding: 4px 0;
}
.phui-form-iconset-table .phui-form-iconset-button-cell {
padding: 4px 8px;
}
.aphront-form-preview-hidden {
opacity: 0.5;
}
.aphront-form-error .phui-icon-view {
float: right;
color: {$lightgreyborder};
font-size: 20px;
}
.device-desktop .aphront-form-error .phui-icon-view:hover {
color: {$red};
}
.phui-form-static-action {
- padding: 4px;
+ height: 28px;
+ line-height: 28px;
color: {$bluetext};
}
.phuix-form-checkbox-action {
padding: 4px;
color: {$bluetext};
}
.phuix-form-checkbox-action input[type=checkbox] {
margin: 4px 0;
}
.phuix-form-checkbox-label {
margin-left: 4px;
}
diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css
index 478dde215..6cafb3e33 100644
--- a/webroot/rsrc/css/phui/phui-header-view.css
+++ b/webroot/rsrc/css/phui/phui-header-view.css
@@ -1,388 +1,388 @@
/**
* @provides phui-header-view-css
*/
.phui-header-shell {
border-bottom: 1px solid {$thinblueborder};
overflow: hidden;
padding: 0 4px 12px;
}
.phui-header-view {
display: table;
width: 100%
}
.phui-header-row {
display: table-row;
}
.phui-header-col1 {
display: table-cell;
vertical-align: middle;
width: 62px;
}
.phui-header-col2 {
display: table-cell;
vertical-align: middle;
word-break: break-word;
}
.phui-header-col3 {
display: table-cell;
vertical-align: middle;
}
body .phui-header-shell.phui-header-no-background {
background-color: transparent;
border: none;
}
body .phui-header-shell.phui-bleed-header {
background-color: #fff;
border-bottom: 1px solid {$thinblueborder};
width: auto;
margin: 16px;
}
body .phui-header-shell.phui-bleed-header
.phui-header-view {
padding: 8px 24px 8px 0;
color: {$darkbluetext};
}
.phui-header-shell + .phabricator-form-view {
border-top-width: 0;
}
.phui-property-list-view + .diviner-document-section {
margin-top: -1px;
}
.phui-header-view {
position: relative;
font-size: {$normalfontsize};
}
.phui-header-header {
font-size: 16px;
line-height: 24px;
color: {$darkbluetext};
}
.phui-header-header .phui-header-icon {
margin-right: 8px;
color: {$lightbluetext};
/* This allows the header text to be triple-clicked to select it in Firefox,
see T10905 for discussion. */
display: inline;
}
.phui-object-box .phui-header-tall .phui-header-header,
.phui-document-view .phui-header-tall .phui-header-header {
font-size: 18px;
}
.phui-header-view .phui-header-header a {
color: {$darkbluetext};
}
.phui-box-blue-property .phui-header-view .phui-header-header a {
color: {$bluetext};
}
.device-desktop .phui-header-view .phui-header-header a:hover {
text-decoration: none;
color: {$blue};
}
.phui-header-view .phui-header-action-links {
float: right;
}
.phui-object-box .phui-header-view .phui-header-action-links {
margin-right: 4px;
font-size: {$normalfontsize};
}
.phui-header-action-link {
margin-bottom: 4px;
margin-top: 4px;
float: right;
}
.device-phone .phui-header-action-link .phui-button-text {
visibility: hidden;
width: 0;
margin-left: 8px;
}
.device-phone .phui-header-action-link.button .phui-icon-view {
width: 12px;
text-align: center;
}
.phui-header-divider {
margin: 0 4px;
font-weight: normal;
color: {$lightbluetext};
}
.phui-header-tags {
margin-left: 12px;
font-size: {$normalfontsize};
}
.phui-header-tags .phui-tag-view {
margin-left: 4px;
}
.phui-header-image {
display: inline-block;
background-repeat: no-repeat;
background-size: 100%;
width: 50px;
height: 50px;
border-radius: 3px;
}
.phui-header-image-href {
position: relative;
display: block;
}
.phui-header-image-edit {
display: none;
}
.device-desktop .phui-header-image-href:hover .phui-header-image-edit {
display: block;
position: absolute;
left: 0;
background: rgba({$alphablack},0.4);
color: #fff;
font-weight: normal;
bottom: 4px;
padding: 4px 8px;
font-size: 12px;
}
.device-desktop .phui-header-image-edit:hover {
text-decoration: underline;
}
.phui-header-subheader {
font-weight: normal;
font-size: {$biggerfontsize};
margin-top: 8px;
}
.phui-header-subheader .phui-icon-view {
margin-right: 4px;
}
.phui-header-subheader .phui-tag-view span.phui-icon-view,
.phui-header-subheader .policy-header-callout span.phui-icon-view {
display: inline-block;
margin: -2px 4px -2px 0;
font-size: 15px;
}
.phui-header-subheader,
.phui-header-subheader .policy-link {
color: {$darkbluetext};
}
.policy-header-callout,
.phui-header-subheader .phui-tag-core {
padding: 3px 9px;
border-radius: 3px;
background: rgba({$alphablue}, 0.1);
margin-right: 8px;
-webkit-font-smoothing: auto;
border-color: transparent;
}
.phui-header-subheader .phui-tag-view,
.phui-header-subheader .phui-tag-type-shade .phui-tag-core {
border: none;
font-weight: normal;
-webkit-font-smoothing: auto;
}
.policy-header-callout.policy-adjusted-weaker {
background: {$sh-greenbackground};
}
.policy-header-callout.policy-adjusted-weaker .policy-link,
.policy-header-callout.policy-adjusted-weaker .phui-icon-view {
color: {$sh-greentext};
}
.policy-header-callout.policy-adjusted-stronger {
background: {$sh-redbackground};
}
.policy-header-callout.policy-adjusted-stronger .policy-link,
.policy-header-callout.policy-adjusted-stronger .phui-icon-view {
color: {$sh-redtext};
}
.policy-header-callout.policy-adjusted-different {
background: {$sh-orangebackground};
}
.policy-header-callout.policy-adjusted-different .policy-link,
.policy-header-callout.policy-adjusted-different .phui-icon-view {
color: {$sh-orangetext};
}
.policy-header-callout.policy-adjusted-special {
background: {$sh-indigobackground};
}
.policy-header-callout.policy-adjusted-special .policy-link,
.policy-header-callout.policy-adjusted-special .phui-icon-view {
color: {$sh-indigotext};
}
.policy-header-callout .policy-space-container {
font-weight: bold;
color: {$sh-redtext};
}
.policy-header-callout .policy-tier-separator {
padding: 0 0 0 4px;
color: {$lightgreytext};
}
-
.phui-header-action-links .phui-mobile-menu {
display: none;
}
.device .phui-header-action-links .phui-mobile-menu {
display: inline-block;
}
.phui-header-action-list {
float: right;
}
.phui-header-action-list li {
margin: 0 0 0 8px;
float: right;
}
.phui-header-action-list .phui-header-action-item .phui-icon-view {
height: 18px;
width: 16px;
font-size: 16px;
line-height: 20px;
display: block;
}
.spaces-name {
color: {$lightbluetext};
}
.phui-object-box .phui-header-tall .spaces-name {
font-size: 18px;
}
.spaces-name .phui-handle,
-.spaces-name a.phui-handle {
+.spaces-name a.phui-handle,
+.phui-profile-header.phui-header-shell .spaces-name .phui-handle {
color: {$sh-redtext};
}
.device-desktop .spaces-name a.phui-handle:hover {
color: {$sh-redtext};
text-decoration: underline;
}
/*** Profile Header ***********************************************************/
.phui-profile-header {
padding: 24px 20px 20px 24px;
}
.device-phone .phui-profile-header {
padding: 12px;
}
.phui-profile-header.phui-header-shell {
margin: 0;
border: none;
}
.phui-profile-header .phui-header-image {
height: 80px;
width: 80px;
}
.phui-profile-header .phui-header-col1 {
width: 96px;
}
.phui-profile-header .phui-header-subheader {
margin-top: 12px;
}
.phui-profile-header.phui-header-shell .phui-header-header {
font-size: 24px;
color: {$blacktext};
}
.phui-profile-header.phui-header-shell .phui-header-header a {
color: {$blacktext};
}
.phui-header-view .phui-tag-indigo a {
color: {$sh-indigotext};
}
.phui-policy-section-view {
margin-bottom: 24px;
}
.phui-policy-section-view-header {
background: {$bluebackground};
border-bottom: 1px solid {$lightblueborder};
padding: 4px 8px;
color: {$darkbluetext};
margin-bottom: 8px;
}
.phui-policy-section-view-header-text {
font-weight: bold;
}
.phui-policy-section-view-header .phui-icon-view {
margin-right: 8px;
}
.phui-policy-section-view-link {
float: right;
}
.phui-policy-section-view-link .phui-icon-view {
color: {$bluetext};
}
.phui-policy-section-view-hint {
color: {$greytext};
background: {$lightbluebackground};
padding: 8px;
}
.phui-policy-section-view-body {
padding: 0 12px;
}
.phui-policy-section-view-inactive-rule {
color: {$greytext};
}
diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css
index 15632c902..ed7fb95a7 100644
--- a/webroot/rsrc/css/phui/phui-property-list-view.css
+++ b/webroot/rsrc/css/phui/phui-property-list-view.css
@@ -1,327 +1,328 @@
/**
* @provides phui-property-list-view-css
*/
.phui-property-list-view .keyboard-shortcuts-available {
float: right;
height: 16px;
margin: 12px 10px -28px 0px;
padding: 0px 20px 0px 0px;
vertical-align: middle;
color: {$greytext};
text-align: right;
font-size: {$smallestfontsize};
background:
url('/rsrc/image/icon/fatcow/key_question.png') right center no-repeat;
}
.device .keyboard-shortcuts-available {
display: none;
}
.phui-property-group-noninitial,
.phui-property-list-section-noninitial {
border-color: {$thinblueborder};
border-style: solid;
border-width: 1px 0 0;
}
.device-desktop .phui-property-list-container {
padding: 12px 0 12px 0;
width: 100%;
}
.device .phui-property-list-container {
padding: 12px 0 4px 0;
}
.phui-property-list-key {
color: {$bluetext};
font-weight: bold;
overflow: hidden;
white-space: nowrap;
}
.device-desktop .phui-property-list-key {
width: 12%;
margin-left: 1%;
text-align: right;
float: left;
clear: left;
margin-bottom: 4px;
}
.device-desktop .phui-property-list-has-actions .phui-property-list-key {
width: 18%;
}
.phui-property-list-properties-wrap.phui-property-list-stacked {
width: auto;
float: none;
}
.device .phui-property-list-key,
div.phui-property-list-stacked .phui-property-list-properties
.phui-property-list-key {
padding-left: 4px;
text-align: left;
margin-left: 0;
width: auto;
float: none;
}
.phui-property-list-value {
color: {$darkgreytext};
}
.device-desktop .phui-property-list-value {
width: 84%;
margin-left: 1%;
float: left;
margin-bottom: 4px;
word-wrap: break-word;
}
.device-desktop .phui-property-list-has-actions .phui-property-list-value {
width: 78%;
}
.device .phui-property-list-value,
.phui-property-list-stacked .phui-property-list-properties
.phui-property-list-value {
padding: 0 8px;
margin-bottom: 8px;
width: auto;
word-break: break-word;
float: none;
}
.phui-property-list-section-header {
color: {$bluetext};
padding: 16px 4px 0px;
text-transform: uppercase;
font-weight: 700;
border-color: {$thinblueborder};
border-style: solid;
border-width: 1px 0 0;
}
.phui-property-list-container + .phui-property-list-text-content {
border-color: {$thinblueborder};
border-style: solid;
border-width: 1px 0 0;
}
.phui-property-list-section-noninitial .phui-property-list-section-header {
border-top: none;
}
.device .phui-property-list-section-header {
padding-left: 4px;
}
.phui-property-list-section-header-icon .phui-icon-view {
display: inline-block;
margin: -2px 4px -2px 0;
}
.phui-property-list-text-content {
padding: 16px 4px;
overflow: hidden;
}
.phui-property-list-raw-content {
padding: 0px;
overflow: hidden;
}
/* In the common case where we immediately follow a header, move back up 30px
so we snuggle next to the header. */
.device-desktop .phui-header-view
+ .phabricator-action-list-view {
margin-top: -30px;
}
.device-desktop .phui-header-view
+ .phabricator-action-list-view
+ .phui-property-list-view {
margin-top: 0px;
}
/* When tags appear in property lists, give them a little more vertical
spacing. */
.phui-property-list-value .phui-tag-view {
margin: 2px 0;
white-space: pre-wrap;
}
.phui-property-list-has-actions .phui-property-list-properties-wrap {
float: left;
width: 78%;
}
.device .phui-property-list-properties-wrap {
width: auto;
border: none;
float: none;
overflow: auto;
}
.phui-property-list-actions {
width: 20%;
float: right;
margin-right: 12px;
border-left: 1px solid {$thinblueborder};
}
!print .phui-property-list-actions {
display: none;
}
.device .phui-property-list-actions {
float: none;
width: auto;
margin: -12px 0 12px 0;
border: none;
}
.phui-property-list-image-content img {
margin: 20px auto;
background: url('/rsrc/image/checker_light.png');
}
.device-desktop .phui-property-list-image-content img:hover {
background: url('/rsrc/image/checker_dark.png');
}
/* - Dashboards ------------------------------------------------------------ */
.dashboard-panel .phui-property-list-section {
border-left: 1px solid {$lightblueborder};
border-right: 1px solid {$lightblueborder};
border-bottom: 1px solid {$blueborder};
}
.document-engine-image img {
margin: 20px auto;
background: url('/rsrc/image/checker_light.png');
+ max-width: 100%;
}
.device-desktop .document-engine-image img:hover {
background: url('/rsrc/image/checker_dark.png');
}
.document-engine-video video {
margin: 20px auto;
display: block;
max-width: 95%;
}
.document-engine-audio audio {
display: block;
margin: 16px auto;
width: 50%;
min-width: 240px;
}
.document-engine-message {
margin: 20px auto;
text-align: center;
color: {$greytext};
}
.document-engine-error {
margin: 20px;
padding: 12px;
text-align: center;
color: {$redtext};
background: {$sh-redbackground};
}
.document-engine-hexdump {
margin: 20px;
white-space: pre;
}
.document-engine-remarkup {
margin: 20px;
}
.document-engine-pdf {
margin: 20px;
text-align: center;
}
.document-engine-pdf .phabricator-remarkup-embed-layout-link {
text-align: left;
}
.document-engine-text .phabricator-source-code-container {
border: none;
}
.document-engine-jupyter {
overflow: hidden;
margin: 20px;
}
.document-engine-in-flight {
opacity: 0.25;
}
.document-engine-loading {
margin: 20px;
text-align: center;
color: {$lightgreytext};
}
.document-engine-loading .phui-icon-view {
display: block;
font-size: 48px;
color: {$lightgreyborder};
padding: 8px;
}
.jupyter-cell-raw {
white-space: pre-wrap;
background: {$lightgreybackground};
color: {$greytext};
padding: 8px;
}
.jupyter-cell-code {
white-space: pre-wrap;
background: {$lightgreybackground};
padding: 8px;
border: 1px solid {$lightgreyborder};
border-radius: 2px;
}
.jupyter-notebook > tbody > tr > th,
.jupyter-notebook > tbody > tr > td {
padding: 8px;
}
.jupyter-notebook > tbody > tr > th {
white-space: nowrap;
text-align: right;
min-width: 48px;
font-weight: bold;
}
.jupyter-output {
margin: 4px 0;
padding: 8px;
white-space: pre-wrap;
word-break: break-all;
}
.jupyter-output-stderr {
background: {$sh-redbackground};
}
.jupyter-output-html {
background: {$sh-indigobackground};
}
diff --git a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
index a60c91e52..2f3dc59a4 100644
--- a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
+++ b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
@@ -1,529 +1,534 @@
/**
* @requires javelin-dom
* javelin-util
* javelin-stratcom
* javelin-install
* @provides javelin-tokenizer
* @javelin
*/
/**
* A tokenizer is a UI component similar to a text input, except that it
* allows the user to input a list of items ("tokens"), generally from a fixed
* set of results. A familiar example of this UI is the "To:" field of most
* email clients, where the control autocompletes addresses from the user's
* address book.
*
* @{JX.Tokenizer} is built on top of @{JX.Typeahead}, and primarily adds the
* ability to choose multiple items.
*
* To build a @{JX.Tokenizer}, you need to do four things:
*
* 1. Construct it, padding a DOM node for it to attach to. See the constructor
* for more information.
* 2. Build a {@JX.Typeahead} and configure it with setTypeahead().
* 3. Configure any special options you want.
* 4. Call start().
*
* If you do this correctly, the input should suggest items and enter them as
* tokens as the user types.
*
* When the tokenizer is focused, the CSS class `jx-tokenizer-container-focused`
* is added to the container node.
*/
JX.install('Tokenizer', {
construct : function(containerNode) {
this._containerNode = containerNode;
},
events : [
/**
* Emitted when the value of the tokenizer changes, similar to an 'onchange'
* from a <select />.
*/
'change'],
properties : {
limit : null,
renderTokenCallback : null,
browseURI: null,
disabled: false
},
members : {
_containerNode : null,
_root : null,
_frame: null,
_focus : null,
_orig : null,
_typeahead : null,
_tokenid : 0,
_tokens : null,
_tokenMap : null,
_initialValue : null,
_seq : 0,
_lastvalue : null,
_placeholder : null,
start : function() {
if (this.getDisabled()) {
JX.DOM.alterClass(this._containerNode, 'disabled-control', true);
return;
}
if (__DEV__) {
if (!this._typeahead) {
throw new Error(
'JX.Tokenizer.start(): ' +
'No typeahead configured! Use setTypeahead() to provide a ' +
'typeahead.');
}
}
this._orig = JX.DOM.find(this._containerNode, 'input', 'tokenizer-input');
this._tokens = [];
this._tokenMap = {};
try {
this._frame = JX.DOM.findAbove(this._orig, 'div', 'tokenizer-frame');
} catch (e) {
// Ignore, this tokenizer doesn't have a frame.
}
if (this._frame) {
JX.DOM.alterClass(this._frame, 'has-browse', !!this.getBrowseURI());
JX.DOM.listen(
this._frame,
'click',
'tokenizer-browse',
JX.bind(this, this._onbrowse));
}
var focus = this.buildInput(this._orig.value);
this._focus = focus;
var input_container = JX.DOM.scry(
this._containerNode,
'div',
'tokenizer-input-container'
);
input_container = input_container[0] || this._containerNode;
JX.DOM.listen(
focus,
['click', 'focus', 'blur', 'keydown', 'keypress', 'paste'],
null,
JX.bind(this, this.handleEvent));
// NOTE: Safari on the iPhone does not normally delegate click events on
// <div /> tags. This causes the event to fire. We want a click (in this
// case, a touch) anywhere in the div to trigger this event so that we
// can focus the input. Without this, you must tap an arbitrary area on
// the left side of the input to focus it.
//
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
input_container.onclick = JX.bag;
JX.DOM.listen(
input_container,
'click',
null,
JX.bind(
this,
function(e) {
if (e.getNode('remove')) {
this._remove(e.getNodeData('token').key, true);
} else if (e.getTarget() == this._root) {
this.focus();
}
}));
var root = JX.$N('div');
root.id = this._orig.id;
JX.DOM.alterClass(root, 'jx-tokenizer', true);
root.style.cursor = 'text';
this._root = root;
root.appendChild(focus);
var typeahead = this._typeahead;
typeahead.setInputNode(this._focus);
typeahead.start();
setTimeout(JX.bind(this, function() {
var container = this._orig.parentNode;
JX.DOM.setContent(container, root);
var map = this._initialValue || {};
for (var k in map) {
this.addToken(k, map[k]);
}
JX.DOM.appendContent(
root,
JX.$N('div', {style: {clear: 'both'}})
);
this._redraw();
}), 0);
},
setInitialValue : function(map) {
this._initialValue = map;
return this;
},
setTypeahead : function(typeahead) {
typeahead.setAllowNullSelection(false);
typeahead.removeListener();
typeahead.listen(
'choose',
JX.bind(this, function(result) {
JX.Stratcom.context().prevent();
if (this.addToken(result.rel, result.name)) {
if (this.shouldHideResultsOnChoose()) {
this._typeahead.hide();
}
this._typeahead.clear();
this._redraw();
this.focus();
}
})
);
typeahead.listen(
'query',
JX.bind(
this,
function(query) {
// TODO: We should emit a 'query' event here to allow the caller to
// generate tokens on the fly, e.g. email addresses or other freeform
// or algorithmic tokens.
// Then do this if something handles the event.
// this._focus.value = '';
// this._redraw();
// this.focus();
if (query.length) {
// Prevent this event if there's any text, so that we don't submit
// the form (either we created a token or we failed to create a
// token; in either case we shouldn't submit). If the query is
// empty, allow the event so that the form submission takes place.
JX.Stratcom.context().prevent();
}
}));
this._typeahead = typeahead;
return this;
},
shouldHideResultsOnChoose : function() {
return true;
},
handleEvent : function(e) {
this._typeahead.handleEvent(e);
if (e.getPrevented()) {
return;
}
if (e.getType() == 'click') {
if (e.getTarget() == this._root) {
this.focus();
e.prevent();
return;
}
} else if (e.getType() == 'keydown') {
this._onkeydown(e);
} else if (e.getType() == 'blur') {
this._didblur();
// Explicitly update the placeholder since we just wiped the field
// value.
this._typeahead.updatePlaceholder();
} else if (e.getType() == 'focus') {
this._didfocus();
} else if (e.getType() == 'paste') {
setTimeout(JX.bind(this, this._redraw), 0);
}
},
refresh : function() {
this._redraw(true);
return this;
},
_redraw : function(force) {
// If there are tokens in the tokenizer, never show a placeholder.
// Otherwise, show one if one is configured.
if (JX.keys(this._tokenMap).length) {
this._typeahead.setPlaceholder(null);
} else {
this._typeahead.setPlaceholder(this._placeholder);
}
var focus = this._focus;
if (focus.value === this._lastvalue && !force) {
return;
}
this._lastvalue = focus.value;
var metrics = JX.DOM.textMetrics(
this._focus,
'jx-tokenizer-metrics');
metrics.y = null;
metrics.x += 24;
metrics.setDim(focus);
// NOTE: Once, long ago, we set "focus.value = focus.value;" here to fix
// an issue with copy/paste in Firefox not redrawing correctly. However,
// this breaks input of Japanese glyphs in Chrome, and I can't reproduce
// the original issue in modern Firefox.
//
// If future changes muck around with things here, test that Japanese
// inputs still work. Example:
//
// - Switch to Hiragana mode.
// - Type "ni".
// - This should produce a glyph, not the value "n".
//
// With the assignment, Chrome loses the partial input on the "n" when
// the value is assigned.
},
setPlaceholder : function(string) {
this._placeholder = string;
return this;
},
addToken : function(key, value) {
if (key in this._tokenMap) {
return false;
}
var focus = this._focus;
var root = this._root;
var token = this.buildToken(key, value);
this._tokenMap[key] = {
value : value,
key : key,
node : token
};
this._tokens.push(key);
root.insertBefore(token, focus);
this._didChangeValue();
return true;
},
removeToken : function(key) {
return this._remove(key, false);
},
buildInput: function(value) {
return JX.$N('input', {
className: 'jx-tokenizer-input',
type: 'text',
autocomplete: 'off',
value: value
});
},
/**
* Generate a token based on a key and value. The "token" and "remove"
* sigils are observed by a listener in start().
*/
buildToken: function(key, value) {
var input = JX.$N('input', {
type: 'hidden',
value: key,
name: this._orig.name + '[' + (this._seq++) + ']'
});
var remove = JX.$N('a', {
className: 'jx-tokenizer-x',
sigil: 'remove'
}, '\u00d7'); // U+00D7 multiplication sign
var display_token = value;
var attrs = {
className: 'jx-tokenizer-token',
sigil: 'token',
meta: {key: key}
};
var container = JX.$N('a', attrs);
var render_callback = this.getRenderTokenCallback();
if (render_callback) {
display_token = render_callback(value, key, container);
}
JX.DOM.setContent(container, [display_token, input, remove]);
return container;
},
getTokens : function() {
var result = {};
for (var key in this._tokenMap) {
result[key] = this._tokenMap[key].value;
}
return result;
},
_onkeydown : function(e) {
var raw = e.getRawEvent();
if (raw.ctrlKey || raw.metaKey || raw.altKey) {
return;
}
switch (e.getSpecialKey()) {
case 'tab':
var completed = this._typeahead.submit();
if (!completed) {
this._focus.value = '';
}
break;
case 'delete':
if (!this._focus.value.length) {
+ // In unusual cases, it's possible for us to end up with a token
+ // that has the empty string ("") as a value. Support removal of
+ // this unusual token.
+
var tok;
- while ((tok = this._tokens.pop())) {
+ while (this._tokens.length) {
+ tok = this._tokens.pop();
if (this._remove(tok, true)) {
break;
}
}
}
break;
case 'return':
// Don't subject this to token limits.
break;
default:
if (this.getLimit() &&
JX.keys(this._tokenMap).length == this.getLimit()) {
e.prevent();
}
setTimeout(JX.bind(this, this._redraw), 0);
break;
}
},
_remove : function(index, focus) {
if (!this._tokenMap[index]) {
return false;
}
JX.DOM.remove(this._tokenMap[index].node);
delete this._tokenMap[index];
this._redraw(true);
focus && this.focus();
this._didChangeValue();
return true;
},
_didChangeValue: function() {
if (this.getBrowseURI()) {
var button = JX.DOM.find(this._frame, 'a', 'tokenizer-browse');
JX.DOM.alterClass(button, 'disabled', !!this._shouldLockBrowse());
}
this.invoke('change', this);
},
_shouldLockBrowse: function() {
var limit = this.getLimit();
if (!limit) {
// If there's no limit, never lock the browse button.
return false;
}
if (limit == 1) {
// If the limit is 1, we'll replace the current token if the
// user selects a new one, so we never need to lock the button.
return false;
}
if (limit > JX.keys(this.getTokens()).length) {
return false;
}
return true;
},
focus : function() {
var focus = this._focus;
JX.DOM.show(focus);
// NOTE: We must fire this focus event immediately (during event
// handling) for the iPhone to bring up the keyboard. Previously this
// focus was wrapped in setTimeout(), but it's unclear why that was
// necessary. If this is adjusted later, make sure tapping the inactive
// area of the tokenizer to focus it on the iPhone still brings up the
// keyboard.
JX.DOM.focus(focus);
},
_didfocus : function() {
JX.DOM.alterClass(
this._containerNode,
'jx-tokenizer-container-focused',
true);
},
_didblur : function() {
JX.DOM.alterClass(
this._containerNode,
'jx-tokenizer-container-focused',
false);
this._focus.value = '';
this._redraw();
},
_onbrowse: function(e) {
e.kill();
var uri = this.getBrowseURI();
if (!uri) {
return;
}
if (this._shouldLockBrowse()) {
return;
}
new JX.Workflow(uri, {exclude: JX.keys(this.getTokens()).join(',')})
.setHandler(
JX.bind(this, function(r) {
var source = this._typeahead.getDatasource();
source.addResult(r.token);
var result = source.getResult(r.key);
// If we have a limit of 1 token, replace the current token with
// the new token if we currently have a token.
if (this.getLimit() == 1) {
for (var k in this.getTokens()) {
this.removeToken(k);
}
}
this.addToken(r.key, result.name);
this.focus();
}))
.start();
}
}
});
diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js
index 5eba051d9..a95267c0a 100644
--- a/webroot/rsrc/js/application/diff/DiffChangesetList.js
+++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js
@@ -1,1872 +1,1885 @@
/**
* @provides phabricator-diff-changeset-list
* @requires javelin-install
* phuix-button-view
* @javelin
*/
JX.install('DiffChangesetList', {
construct: function() {
this._changesets = [];
var onload = JX.bind(this, this._ifawake, this._onload);
JX.Stratcom.listen('click', 'differential-load', onload);
var onmore = JX.bind(this, this._ifawake, this._onmore);
JX.Stratcom.listen('click', 'show-more', onmore);
var onmenu = JX.bind(this, this._ifawake, this._onmenu);
JX.Stratcom.listen('click', 'differential-view-options', onmenu);
var oncollapse = JX.bind(this, this._ifawake, this._oncollapse, true);
JX.Stratcom.listen('click', 'hide-inline', oncollapse);
var onexpand = JX.bind(this, this._ifawake, this._oncollapse, false);
JX.Stratcom.listen('click', 'reveal-inline', onexpand);
var onedit = JX.bind(this, this._ifawake, this._onaction, 'edit');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-edit'],
onedit);
var ondone = JX.bind(this, this._ifawake, this._onaction, 'done');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-done'],
ondone);
var ondelete = JX.bind(this, this._ifawake, this._onaction, 'delete');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-delete'],
ondelete);
var onreply = JX.bind(this, this._ifawake, this._onaction, 'reply');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-reply'],
onreply);
var onresize = JX.bind(this, this._ifawake, this._onresize);
JX.Stratcom.listen('resize', null, onresize);
var onscroll = JX.bind(this, this._ifawake, this._onscroll);
JX.Stratcom.listen('scroll', null, onscroll);
var onselect = JX.bind(this, this._ifawake, this._onselect);
JX.Stratcom.listen(
'mousedown',
['differential-inline-comment', 'differential-inline-header'],
onselect);
var onhover = JX.bind(this, this._ifawake, this._onhover);
JX.Stratcom.listen(
['mouseover', 'mouseout'],
'differential-inline-comment',
onhover);
var onrangedown = JX.bind(this, this._ifawake, this._onrangedown);
JX.Stratcom.listen(
'mousedown',
['differential-changeset', 'tag:th'],
onrangedown);
var onrangemove = JX.bind(this, this._ifawake, this._onrangemove);
JX.Stratcom.listen(
['mouseover', 'mouseout'],
['differential-changeset', 'tag:th'],
onrangemove);
var onrangeup = JX.bind(this, this._ifawake, this._onrangeup);
JX.Stratcom.listen(
'mouseup',
null,
onrangeup);
},
properties: {
translations: null,
inlineURI: null,
- inlineListURI: null
+ inlineListURI: null,
+ isStandalone: false
},
members: {
_initialized: false,
_asleep: true,
_changesets: null,
_cursorItem: null,
_focusNode: null,
_focusStart: null,
_focusEnd: null,
_hoverNode: null,
_hoverInline: null,
_hoverOrigin: null,
_hoverTarget: null,
_rangeActive: false,
_rangeOrigin: null,
_rangeTarget: null,
_bannerNode: null,
_unsavedButton: null,
_unsubmittedButton: null,
_doneButton: null,
_doneMode: null,
_dropdownMenu: null,
_menuButton: null,
_menuItems: null,
sleep: function() {
this._asleep = true;
this._redrawFocus();
this._redrawSelection();
this.resetHover();
this._bannerChangeset = null;
this._redrawBanner();
},
wake: function() {
this._asleep = false;
this._redrawFocus();
this._redrawSelection();
this._bannerChangeset = null;
this._redrawBanner();
if (this._initialized) {
return;
}
this._initialized = true;
var pht = this.getTranslations();
+ // We may be viewing the normal "/D123" view (with all the changesets)
+ // or the standalone view (with just one changeset). In the standalone
+ // view, some options (like jumping to next or previous file) do not
+ // make sense and do not function.
+ var standalone = this.getIsStandalone();
+
var label;
label = pht('Jump to next change.');
this._installJumpKey('j', label, 1);
label = pht('Jump to previous change.');
this._installJumpKey('k', label, -1);
- label = pht('Jump to next file.');
- this._installJumpKey('J', label, 1, 'file');
+ if (!standalone) {
+ label = pht('Jump to next file.');
+ this._installJumpKey('J', label, 1, 'file');
- label = pht('Jump to previous file.');
- this._installJumpKey('K', label, -1, 'file');
+ label = pht('Jump to previous file.');
+ this._installJumpKey('K', label, -1, 'file');
+ }
label = pht('Jump to next inline comment.');
this._installJumpKey('n', label, 1, 'comment');
label = pht('Jump to previous inline comment.');
this._installJumpKey('p', label, -1, 'comment');
label = pht('Jump to next inline comment, including collapsed comments.');
this._installJumpKey('N', label, 1, 'comment', true);
label = pht(
'Jump to previous inline comment, including collapsed comments.');
this._installJumpKey('P', label, -1, 'comment', true);
- label = pht('Hide or show the current file.');
- this._installKey('h', label, this._onkeytogglefile);
+ if (!standalone) {
+ label = pht('Hide or show the current file.');
+ this._installKey('h', label, this._onkeytogglefile);
- label = pht('Jump to the table of contents.');
- this._installKey('t', label, this._ontoc);
+ label = pht('Jump to the table of contents.');
+ this._installKey('t', label, this._ontoc);
+ }
label = pht('Reply to selected inline comment or change.');
this._installKey('r', label, JX.bind(this, this._onkeyreply, false));
label = pht('Reply and quote selected inline comment.');
this._installKey('R', label, JX.bind(this, this._onkeyreply, true));
label = pht('Edit selected inline comment.');
this._installKey('e', label, this._onkeyedit);
label = pht('Mark or unmark selected inline comment as done.');
this._installKey('w', label, this._onkeydone);
label = pht('Collapse or expand inline comment.');
this._installKey('q', label, this._onkeycollapse);
label = pht('Hide or show all inline comments.');
this._installKey('A', label, this._onkeyhideall);
},
isAsleep: function() {
return this._asleep;
},
newChangesetForNode: function(node) {
var changeset = JX.DiffChangeset.getForNode(node);
this._changesets.push(changeset);
changeset.setChangesetList(this);
return changeset;
},
getChangesetForNode: function(node) {
return JX.DiffChangeset.getForNode(node);
},
getInlineByID: function(id) {
var inline = null;
for (var ii = 0; ii < this._changesets.length; ii++) {
inline = this._changesets[ii].getInlineByID(id);
if (inline) {
break;
}
}
return inline;
},
_ifawake: function(f) {
// This function takes another function and only calls it if the
// changeset list is awake, so we basically just ignore events when we
// are asleep. This may move up the stack at some point as we do more
// with Quicksand/Sheets.
if (this.isAsleep()) {
return;
}
return f.apply(this, [].slice.call(arguments, 1));
},
_onload: function(e) {
var data = e.getNodeData('differential-load');
// NOTE: We can trigger a load from either an explicit "Load" link on
// the changeset, or by clicking a link in the table of contents. If
// the event was a table of contents link, we let the anchor behavior
// run normally.
if (data.kill) {
e.kill();
}
var node = JX.$(data.id);
var changeset = this.getChangesetForNode(node);
changeset.load();
// TODO: Move this into Changeset.
var routable = changeset.getRoutable();
if (routable) {
routable.setPriority(2000);
}
},
_installKey: function(key, label, handler) {
handler = JX.bind(this, this._ifawake, handler);
return new JX.KeyboardShortcut(key, label)
.setHandler(handler)
.register();
},
_installJumpKey: function(key, label, delta, filter, show_collapsed) {
filter = filter || null;
var options = {
filter: filter,
collapsed: show_collapsed
};
var handler = JX.bind(this, this._onjumpkey, delta, options);
return this._installKey(key, label, handler);
},
_ontoc: function(manager) {
var toc = JX.$('toc');
manager.scrollTo(toc);
},
getSelectedInline: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
return cursor.target;
}
}
return null;
},
_onkeyreply: function(is_quote) {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canReply()) {
this.setFocus(null);
var text;
if (is_quote) {
text = inline.getRawText();
text = '> ' + text.replace(/\n/g, '\n> ') + '\n\n';
} else {
text = '';
}
inline.reply(text);
return;
}
}
// If the keyboard cursor is selecting a range of lines, we may have
// a mixture of old and new changes on the selected rows. It is not
// entirely unambiguous what the user means when they say they want
// to reply to this, but we use this logic: reply on the new file if
// there are any new lines. Otherwise (if there are only removed
// lines) reply on the old file.
if (cursor.type == 'change') {
var origin = cursor.nodes.begin;
var target = cursor.nodes.end;
// The "origin" and "target" are entire rows, but we need to find
// a range of "<th />" nodes to actually create an inline, so go
// fishing.
var old_list = [];
var new_list = [];
var row = origin;
while (row) {
var header = row.firstChild;
while (header) {
if (JX.DOM.isType(header, 'th')) {
if (header.className.indexOf('old') !== -1) {
old_list.push(header);
} else if (header.className.indexOf('new') !== -1) {
new_list.push(header);
}
}
header = header.nextSibling;
}
if (row == target) {
break;
}
row = row.nextSibling;
}
var use_list;
if (new_list.length) {
use_list = new_list;
} else {
use_list = old_list;
}
var src = use_list[0];
var dst = use_list[use_list.length - 1];
cursor.changeset.newInlineForRange(src, dst);
this.setFocus(null);
return;
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment or change to reply to.'));
},
_onkeyedit: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canEdit()) {
this.setFocus(null);
inline.edit();
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to edit.'));
},
_onkeydone: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canDone()) {
this.setFocus(null);
inline.toggleDone();
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to mark done.'));
},
_onkeytogglefile: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'file') {
cursor.changeset.toggleVisibility();
return;
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a file to hide or show.'));
},
_onkeycollapse: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canCollapse()) {
this.setFocus(null);
inline.setCollapsed(!inline.isCollapsed());
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to hide.'));
},
_onkeyhideall: function() {
var inlines = this._getInlinesByType();
if (inlines.visible.length) {
this._toggleInlines('all');
} else {
this._toggleInlines('show');
}
},
_warnUser: function(message) {
new JX.Notification()
.setContent(message)
.alterClassName('jx-notification-alert', true)
.setDuration(3000)
.show();
},
_onjumpkey: function(delta, options) {
var state = this._getSelectionState();
var filter = options.filter || null;
var collapsed = options.collapsed || false;
var wrap = options.wrap || false;
var attribute = options.attribute || null;
var show = options.show || false;
var cursor = state.cursor;
var items = state.items;
// If there's currently no selection and the user tries to go back,
// don't do anything.
if ((cursor === null) && (delta < 0)) {
return;
}
var did_wrap = false;
while (true) {
if (cursor === null) {
cursor = 0;
} else {
cursor = cursor + delta;
}
// If we've gone backward past the first change, bail out.
if (cursor < 0) {
return;
}
// If we've gone forward off the end of the list, figure out where we
// should end up.
if (cursor >= items.length) {
if (!wrap) {
// If we aren't wrapping around, we're done.
return;
}
if (did_wrap) {
// If we're already wrapped around, we're done.
return;
}
// Otherwise, wrap the cursor back to the top.
cursor = 0;
did_wrap = true;
}
// If we're selecting things of a particular type (like only files)
// and the next item isn't of that type, move past it.
if (filter !== null) {
if (items[cursor].type !== filter) {
continue;
}
}
// If the item is collapsed, don't select it when iterating with jump
// keys. It can still potentially be selected in other ways.
if (!collapsed) {
if (items[cursor].collapsed) {
continue;
}
}
// If the item has been deleted, don't select it when iterating. The
// cursor may remain on it until it is removed.
if (items[cursor].deleted) {
continue;
}
// If we're selecting things with a particular attribute, like
// "unsaved", skip items without the attribute.
if (attribute !== null) {
if (!(items[cursor].attributes || {})[attribute]) {
continue;
}
}
// If this item is a hidden inline but we're clicking a button which
// selects inlines of a particular type, make it visible again.
if (items[cursor].hidden) {
if (!show) {
continue;
}
items[cursor].target.setHidden(false);
}
// Otherwise, we've found a valid item to select.
break;
}
this._setSelectionState(items[cursor], true);
},
_getSelectionState: function() {
var items = this._getSelectableItems();
var cursor = null;
if (this._cursorItem !== null) {
for (var ii = 0; ii < items.length; ii++) {
var item = items[ii];
if (this._cursorItem.target === item.target) {
cursor = ii;
break;
}
}
}
return {
cursor: cursor,
items: items
};
},
_setSelectionState: function(item, scroll) {
this._cursorItem = item;
this._redrawSelection(scroll);
return this;
},
_redrawSelection: function(scroll) {
var cursor = this._cursorItem;
if (!cursor) {
this.setFocus(null);
return;
}
// If this item has been removed from the document (for example: create
// a new empty comment, then use the "Unsaved" button to select it, then
// cancel it), we can still keep the cursor here but do not want to show
// a selection reticle over an invisible node.
if (cursor.deleted) {
this.setFocus(null);
return;
}
this.setFocus(cursor.nodes.begin, cursor.nodes.end);
if (scroll) {
var pos = JX.$V(cursor.nodes.begin);
JX.DOM.scrollToPosition(0, pos.y - 60);
}
return this;
},
redrawCursor: function() {
// NOTE: This is setting the cursor to the current cursor. Usually, this
// would have no effect.
// However, if the old cursor pointed at an inline and the inline has
// been edited so the rows have changed, this updates the cursor to point
// at the new inline with the proper rows for the current state, and
// redraws the reticle correctly.
var state = this._getSelectionState();
if (state.cursor !== null) {
this._setSelectionState(state.items[state.cursor], false);
}
},
_getSelectableItems: function() {
var result = [];
for (var ii = 0; ii < this._changesets.length; ii++) {
var items = this._changesets[ii].getSelectableItems();
for (var jj = 0; jj < items.length; jj++) {
result.push(items[jj]);
}
}
return result;
},
_onhover: function(e) {
if (e.getIsTouchEvent()) {
return;
}
var inline;
if (e.getType() == 'mouseout') {
inline = null;
} else {
inline = this._getInlineForEvent(e);
}
this._setHoverInline(inline);
},
_onmore: function(e) {
e.kill();
var node = e.getNode('differential-changeset');
var changeset = this.getChangesetForNode(node);
var data = e.getNodeData('show-more');
var target = e.getNode('context-target');
changeset.loadContext(data.range, target);
},
_onmenu: function(e) {
var button = e.getNode('differential-view-options');
var data = JX.Stratcom.getData(button);
if (data.menu) {
// We've already built this menu, so we can let the menu itself handle
// the event.
return;
}
e.prevent();
var pht = this.getTranslations();
var node = JX.DOM.findAbove(
button,
'div',
'differential-changeset');
var changeset_list = this;
var changeset = this.getChangesetForNode(node);
var menu = new JX.PHUIXDropdownMenu(button);
var list = new JX.PHUIXActionListView();
var add_link = function(icon, name, href, local) {
if (!href) {
return;
}
var link = new JX.PHUIXActionView()
.setIcon(icon)
.setName(name)
.setHref(href)
.setHandler(function(e) {
if (local) {
window.location.assign(href);
} else {
window.open(href);
}
menu.close();
e.prevent();
});
list.addItem(link);
return link;
};
var reveal_item = new JX.PHUIXActionView()
.setIcon('fa-eye');
list.addItem(reveal_item);
var visible_item = new JX.PHUIXActionView()
.setHandler(function(e) {
e.prevent();
menu.close();
changeset.toggleVisibility();
});
list.addItem(visible_item);
add_link('fa-file-text', pht('Browse in Diffusion'), data.diffusionURI);
add_link('fa-file-o', pht('View Standalone'), data.standaloneURI);
var up_item = new JX.PHUIXActionView()
.setHandler(function(e) {
if (changeset.isLoaded()) {
// Don't let the user swap display modes if a comment is being
// edited, since they might lose their work. See PHI180.
var inlines = changeset.getInlines();
for (var ii = 0; ii < inlines.length; ii++) {
if (inlines[ii].isEditing()) {
changeset_list._warnUser(
pht(
'Finish editing inline comments before changing display ' +
'modes.'));
e.prevent();
menu.close();
return;
}
}
var renderer = changeset.getRenderer();
if (renderer == '1up') {
renderer = '2up';
} else {
renderer = '1up';
}
changeset.setRenderer(renderer);
}
changeset.reload();
e.prevent();
menu.close();
});
list.addItem(up_item);
var encoding_item = new JX.PHUIXActionView()
.setIcon('fa-font')
.setName(pht('Change Text Encoding...'))
.setHandler(function(e) {
var params = {
encoding: changeset.getEncoding()
};
new JX.Workflow('/services/encoding/', params)
.setHandler(function(r) {
changeset.setEncoding(r.encoding);
changeset.reload();
})
.start();
e.prevent();
menu.close();
});
list.addItem(encoding_item);
var highlight_item = new JX.PHUIXActionView()
.setIcon('fa-sun-o')
.setName(pht('Highlight As...'))
.setHandler(function(e) {
var params = {
highlight: changeset.getHighlight()
};
new JX.Workflow('/services/highlight/', params)
.setHandler(function(r) {
changeset.setHighlight(r.highlight);
changeset.reload();
})
.start();
e.prevent();
menu.close();
});
list.addItem(highlight_item);
add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);
add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);
add_link('fa-pencil', pht('Open in Editor'), data.editor, true);
add_link('fa-wrench', pht('Configure Editor'), data.editorConfigure);
menu.setContent(list.getNode());
menu.listen('open', function() {
// When the user opens the menu, check if there are any "Show More"
// links in the changeset body. If there aren't, disable the "Show
// Entire File" menu item since it won't change anything.
var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
if (nodes.length) {
reveal_item
.setDisabled(false)
.setName(pht('Show All Context'))
.setIcon('fa-file-o')
.setHandler(function(e) {
changeset.loadAllContext();
e.prevent();
menu.close();
});
} else {
reveal_item
.setDisabled(true)
.setIcon('fa-file')
.setName(pht('All Context Shown'))
.setHandler(function(e) { e.prevent(); });
}
encoding_item.setDisabled(!changeset.isLoaded());
highlight_item.setDisabled(!changeset.isLoaded());
if (changeset.isLoaded()) {
if (changeset.getRenderer() == '2up') {
up_item
.setIcon('fa-list-alt')
.setName(pht('View Unified'));
} else {
up_item
.setIcon('fa-files-o')
.setName(pht('View Side-by-Side'));
}
} else {
up_item
.setIcon('fa-refresh')
.setName(pht('Load Changes'));
}
visible_item
.setDisabled(true)
.setIcon('fa-expand')
.setName(pht('Can\'t Toggle Unloaded File'));
var diffs = JX.DOM.scry(
JX.$(data.containerID),
'table',
'differential-diff');
if (diffs.length > 1) {
JX.$E(
'More than one node with sigil "differential-diff" was found in "'+
data.containerID+'."');
} else if (diffs.length == 1) {
var diff = diffs[0];
visible_item.setDisabled(false);
if (!changeset.isVisible()) {
visible_item
.setName(pht('Expand File'))
.setIcon('fa-expand');
} else {
visible_item
.setName(pht('Collapse File'))
.setIcon('fa-compress');
}
} else {
// Do nothing when there is no diff shown in the table. For example,
// the file is binary.
}
});
data.menu = menu;
menu.open();
},
_oncollapse: function(is_collapse, e) {
e.kill();
var inline = this._getInlineForEvent(e);
inline.setCollapsed(is_collapse);
},
_onresize: function() {
this._redrawFocus();
this._redrawSelection();
this._redrawHover();
// Force a banner redraw after a resize event. Particularly, this makes
// sure the inline state updates immediately after an inline edit
// operation, even if the changeset itself has not changed.
this._bannerChangeset = null;
this._redrawBanner();
var changesets = this._changesets;
for (var ii = 0; ii < changesets.length; ii++) {
changesets[ii].redrawFileTree();
}
},
_onscroll: function() {
this._redrawBanner();
},
_onselect: function(e) {
// If the user clicked some element inside the header, like an action
// icon, ignore the event. They have to click the header element itself.
if (e.getTarget() !== e.getNode('differential-inline-header')) {
return;
}
var inline = this._getInlineForEvent(e);
if (!inline) {
return;
}
// The user definitely clicked an inline, so we're going to handle the
// event.
e.kill();
this.selectInline(inline);
},
selectInline: function(inline) {
var selection = this._getSelectionState();
var item;
// If the comment the user clicked is currently selected, deselect it.
// This makes it easy to undo things if you clicked by mistake.
if (selection.cursor !== null) {
item = selection.items[selection.cursor];
if (item.target === inline) {
this._setSelectionState(null, false);
return;
}
}
// Otherwise, select the item that the user clicked. This makes it
// easier to resume keyboard operations after using the mouse to do
// something else.
var items = selection.items;
for (var ii = 0; ii < items.length; ii++) {
item = items[ii];
if (item.target === inline) {
this._setSelectionState(item, false);
}
}
},
_onaction: function(action, e) {
e.kill();
var inline = this._getInlineForEvent(e);
var is_ref = false;
// If we don't have a natural inline object, the user may have clicked
// an action (like "Delete") inside a preview element at the bottom of
// the page.
// If they did, try to find an associated normal inline to act on, and
// pretend they clicked that instead. This makes the overall state of
// the page more consistent.
// However, there may be no normal inline (for example, because it is
// on a version of the diff which is not visible). In this case, we
// act by reference.
if (inline === null) {
var data = e.getNodeData('differential-inline-comment');
inline = this.getInlineByID(data.id);
if (inline) {
is_ref = true;
} else {
switch (action) {
case 'delete':
this._deleteInlineByID(data.id);
return;
}
}
}
// TODO: For normal operations, highlight the inline range here.
switch (action) {
case 'edit':
inline.edit();
break;
case 'done':
inline.toggleDone();
break;
case 'delete':
inline.delete(is_ref);
break;
case 'reply':
inline.reply();
break;
}
},
redrawPreview: function() {
// TODO: This isn't the cleanest way to find the preview form, but
// rendering no longer has direct access to it.
var forms = JX.DOM.scry(document.body, 'form', 'transaction-append');
if (forms.length) {
JX.DOM.invoke(forms[0], 'shouldRefresh');
}
// Clear the mouse hover reticle after a substantive edit: we don't get
// a "mouseout" event if the row vanished because of row being removed
// after an edit.
this.resetHover();
},
setFocus: function(node, extended_node) {
this._focusStart = node;
this._focusEnd = extended_node;
this._redrawFocus();
},
_redrawFocus: function() {
var node = this._focusStart;
var extended_node = this._focusEnd || node;
var reticle = this._getFocusNode();
if (!node || this.isAsleep()) {
JX.DOM.remove(reticle);
return;
}
// Outset the reticle some pixels away from the element, so there's some
// space between the focused element and the outline.
var p = JX.Vector.getPos(node);
var s = JX.Vector.getAggregateScrollForNode(node);
p.add(s).add(-4, -4).setPos(reticle);
// Compute the size we need to extend to the full extent of the focused
// nodes.
JX.Vector.getPos(extended_node)
.add(-p.x, -p.y)
.add(JX.Vector.getDim(extended_node))
.add(8, 8)
.setDim(reticle);
JX.DOM.getContentFrame().appendChild(reticle);
},
_getFocusNode: function() {
if (!this._focusNode) {
var node = JX.$N('div', {className : 'keyboard-focus-focus-reticle'});
this._focusNode = node;
}
return this._focusNode;
},
_setHoverInline: function(inline) {
this._hoverInline = inline;
if (inline) {
var changeset = inline.getChangeset();
var changeset_id;
var side = inline.getDisplaySide();
if (side == 'right') {
changeset_id = changeset.getRightChangesetID();
} else {
changeset_id = changeset.getLeftChangesetID();
}
var new_part;
if (inline.isNewFile()) {
new_part = 'N';
} else {
new_part = 'O';
}
var prefix = 'C' + changeset_id + new_part + 'L';
var number = inline.getLineNumber();
var length = inline.getLineLength();
try {
var origin = JX.$(prefix + number);
var target = JX.$(prefix + (number + length));
this._hoverOrigin = origin;
this._hoverTarget = target;
} catch (error) {
// There may not be any nodes present in the document. A case where
// this occurs is when you reply to a ghost inline which was made
// on lines near the bottom of "long.txt" in an earlier diff, and
// the file was later shortened so those lines no longer exist. For
// more details, see T11662.
this._hoverOrigin = null;
this._hoverTarget = null;
}
} else {
this._hoverOrigin = null;
this._hoverTarget = null;
}
this._redrawHover();
},
_setHoverRange: function(origin, target) {
this._hoverOrigin = origin;
this._hoverTarget = target;
this._redrawHover();
},
resetHover: function() {
this._setHoverInline(null);
this._hoverOrigin = null;
this._hoverTarget = null;
},
_redrawHover: function() {
var reticle = this._getHoverNode();
if (!this._hoverOrigin || this.isAsleep()) {
JX.DOM.remove(reticle);
return;
}
JX.DOM.getContentFrame().appendChild(reticle);
var top = this._hoverOrigin;
var bot = this._hoverTarget;
if (JX.$V(top).y > JX.$V(bot).y) {
var tmp = top;
top = bot;
bot = tmp;
}
// Find the leftmost cell that we're going to highlight: this is the next
// <td /> in the row. In 2up views, it should be directly adjacent. In
// 1up views, we may have to skip over the other line number column.
var l = top;
while (JX.DOM.isType(l, 'th')) {
l = l.nextSibling;
}
// Find the rightmost cell that we're going to highlight: this is the
// farthest consecutive, adjacent <td /> in the row. Sometimes the left
// and right nodes are the same (left side of 2up view); sometimes we're
// going to highlight several nodes (copy + code + coverage).
var r = l;
while (r.nextSibling && JX.DOM.isType(r.nextSibling, 'td')) {
r = r.nextSibling;
}
var pos = JX.$V(l)
.add(JX.Vector.getAggregateScrollForNode(l));
var dim = JX.$V(r)
.add(JX.Vector.getAggregateScrollForNode(r))
.add(-pos.x, -pos.y)
.add(JX.Vector.getDim(r));
var bpos = JX.$V(bot)
.add(JX.Vector.getAggregateScrollForNode(bot));
dim.y = (bpos.y - pos.y) + JX.Vector.getDim(bot).y;
pos.setPos(reticle);
dim.setDim(reticle);
JX.DOM.show(reticle);
},
_getHoverNode: function() {
if (!this._hoverNode) {
var attributes = {
className: 'differential-reticle'
};
this._hoverNode = JX.$N('div', attributes);
}
return this._hoverNode;
},
_deleteInlineByID: function(id) {
var uri = this.getInlineURI();
var data = {
op: 'refdelete',
id: id
};
var handler = JX.bind(this, this.redrawPreview);
new JX.Workflow(uri, data)
.setHandler(handler)
.start();
},
_getInlineForEvent: function(e) {
var node = e.getNode('differential-changeset');
if (!node) {
return null;
}
var changeset = this.getChangesetForNode(node);
var inline_row = e.getNode('inline-row');
return changeset.getInlineForRow(inline_row);
},
getLineNumberFromHeader: function(th) {
try {
return parseInt(th.id.match(/^C\d+[ON]L(\d+)$/)[1], 10);
} catch (x) {
return null;
}
},
getDisplaySideFromHeader: function(th) {
return (th.parentNode.firstChild != th) ? 'right' : 'left';
},
_onrangedown: function(e) {
// NOTE: We're allowing "mousedown" from a touch event through so users
// can leave inlines on a single line.
if (e.isRightButton()) {
return;
}
if (this._rangeActive) {
return;
}
var target = e.getTarget();
var number = this.getLineNumberFromHeader(target);
if (!number) {
return;
}
e.kill();
this._rangeActive = true;
this._rangeOrigin = target;
this._rangeTarget = target;
this._setHoverRange(this._rangeOrigin, this._rangeTarget);
},
_onrangemove: function(e) {
if (e.getIsTouchEvent()) {
return;
}
var is_out = (e.getType() == 'mouseout');
var target = e.getTarget();
this._updateRange(target, is_out);
},
_updateRange: function(target, is_out) {
// Don't update the range if this "<th />" doesn't correspond to a line
// number. For instance, this may be a dead line number, like the empty
// line numbers on the left hand side of a newly added file.
var number = this.getLineNumberFromHeader(target);
if (!number) {
return;
}
if (this._rangeActive) {
var origin = this._hoverOrigin;
// Don't update the reticle if we're selecting a line range and the
// "<th />" under the cursor is on the wrong side of the file. You can
// only leave inline comments on the left or right side of a file, not
// across lines on both sides.
var origin_side = this.getDisplaySideFromHeader(origin);
var target_side = this.getDisplaySideFromHeader(target);
if (origin_side != target_side) {
return;
}
// Don't update the reticle if we're selecting a line range and the
// "<th />" under the cursor corresponds to a different file. You can
// only leave inline comments on lines in a single file, not across
// multiple files.
var origin_table = JX.DOM.findAbove(origin, 'table');
var target_table = JX.DOM.findAbove(target, 'table');
if (origin_table != target_table) {
return;
}
}
if (is_out) {
if (this._rangeActive) {
// If we're dragging a range, just leave the state as it is. This
// allows you to drag over something invalid while selecting a
// range without the range flickering or getting lost.
} else {
// Otherwise, clear the current range.
this.resetHover();
}
return;
}
if (this._rangeActive) {
this._rangeTarget = target;
} else {
this._rangeOrigin = target;
this._rangeTarget = target;
}
this._setHoverRange(this._rangeOrigin, this._rangeTarget);
},
_onrangeup: function(e) {
if (!this._rangeActive) {
return;
}
e.kill();
var origin = this._rangeOrigin;
var target = this._rangeTarget;
// If the user dragged a range from the bottom to the top, swap the node
// order around.
if (JX.$V(origin).y > JX.$V(target).y) {
var tmp = target;
target = origin;
origin = tmp;
}
var node = JX.DOM.findAbove(origin, null, 'differential-changeset');
var changeset = this.getChangesetForNode(node);
changeset.newInlineForRange(origin, target);
this._rangeActive = false;
this._rangeOrigin = null;
this._rangeTarget = null;
this.resetHover();
},
_redrawBanner: function() {
// If the inline comment menu is open and we've done a redraw, close it.
// In particular, this makes it close when you scroll the document:
// otherwise, it stays open but the banner moves underneath it.
if (this._dropdownMenu) {
this._dropdownMenu.close();
}
var node = this._getBannerNode();
var changeset = this._getVisibleChangeset();
if (!changeset) {
+ this._bannerChangeset = null;
JX.DOM.remove(node);
return;
}
// Don't do anything if nothing has changed. This seems to avoid some
// flickering issues in Safari, at least.
if (this._bannerChangeset === changeset) {
return;
}
this._bannerChangeset = changeset;
var inlines = this._getInlinesByType();
var unsaved = inlines.unsaved;
var unsubmitted = inlines.unsubmitted;
var undone = inlines.undone;
var done = inlines.done;
var draft_done = inlines.draftDone;
JX.DOM.alterClass(
node,
'diff-banner-has-unsaved',
!!unsaved.length);
JX.DOM.alterClass(
node,
'diff-banner-has-unsubmitted',
!!unsubmitted.length);
JX.DOM.alterClass(
node,
'diff-banner-has-draft-done',
!!draft_done.length);
var pht = this.getTranslations();
var unsaved_button = this._getUnsavedButton();
var unsubmitted_button = this._getUnsubmittedButton();
var done_button = this._getDoneButton();
var menu_button = this._getMenuButton();
if (unsaved.length) {
unsaved_button.setText(unsaved.length + ' ' + pht('Unsaved'));
JX.DOM.show(unsaved_button.getNode());
} else {
JX.DOM.hide(unsaved_button.getNode());
}
if (unsubmitted.length || draft_done.length) {
var any_draft_count = unsubmitted.length + draft_done.length;
unsubmitted_button.setText(any_draft_count + ' ' + pht('Unsubmitted'));
JX.DOM.show(unsubmitted_button.getNode());
} else {
JX.DOM.hide(unsubmitted_button.getNode());
}
if (done.length || undone.length) {
// If you haven't marked any comments as "Done", we just show text
// like "3 Comments". If you've marked at least one done, we show
// "1 / 3 Comments".
var done_text;
if (done.length) {
done_text = [
done.length,
' / ',
(done.length + undone.length),
' ',
pht('Comments')
];
} else {
done_text = [
undone.length,
' ',
pht('Comments')
];
}
done_button.setText(done_text);
JX.DOM.show(done_button.getNode());
// If any comments are not marked "Done", this cycles through the
// missing comments. Otherwise, it cycles through all the saved
// comments.
if (undone.length) {
this._doneMode = 'undone';
} else {
this._doneMode = 'done';
}
} else {
JX.DOM.hide(done_button.getNode());
}
var path_view = [icon, ' ', changeset.getDisplayPath()];
var buttons_attrs = {
className: 'diff-banner-buttons'
};
var buttons_list = [
unsaved_button.getNode(),
unsubmitted_button.getNode(),
done_button.getNode(),
menu_button.getNode()
];
var buttons_view = JX.$N('div', buttons_attrs, buttons_list);
var icon = new JX.PHUIXIconView()
.setIcon(changeset.getIcon())
.getNode();
JX.DOM.setContent(node, [buttons_view, path_view]);
document.body.appendChild(node);
},
_getInlinesByType: function() {
var changesets = this._changesets;
var unsaved = [];
var unsubmitted = [];
var undone = [];
var done = [];
var draft_done = [];
var visible_done = [];
var visible_collapsed = [];
var visible_ghosts = [];
var visible = [];
var hidden = [];
for (var ii = 0; ii < changesets.length; ii++) {
var inlines = changesets[ii].getInlines();
var inline;
var jj;
for (jj = 0; jj < inlines.length; jj++) {
inline = inlines[jj];
if (inline.isDeleted()) {
continue;
}
if (inline.isSynthetic()) {
continue;
}
if (inline.isEditing()) {
unsaved.push(inline);
} else if (!inline.getID()) {
// These are new comments which have been cancelled, and do not
// count as anything.
continue;
} else if (inline.isDraft()) {
unsubmitted.push(inline);
} else {
// NOTE: Unlike other states, an inline may be marked with a
// draft checkmark and still be a "done" or "undone" comment.
if (inline.isDraftDone()) {
draft_done.push(inline);
}
if (!inline.isDone()) {
undone.push(inline);
} else {
done.push(inline);
}
}
}
for (jj = 0; jj < inlines.length; jj++) {
inline = inlines[jj];
if (inline.isDeleted()) {
continue;
}
if (inline.isEditing()) {
continue;
}
if (inline.isHidden()) {
hidden.push(inline);
continue;
}
visible.push(inline);
if (inline.isDone()) {
visible_done.push(inline);
}
if (inline.isCollapsed()) {
visible_collapsed.push(inline);
}
if (inline.isGhost()) {
visible_ghosts.push(inline);
}
}
}
return {
unsaved: unsaved,
unsubmitted: unsubmitted,
undone: undone,
done: done,
draftDone: draft_done,
visibleDone: visible_done,
visibleGhosts: visible_ghosts,
visibleCollapsed: visible_collapsed,
visible: visible,
hidden: hidden
};
},
_getUnsavedButton: function() {
if (!this._unsavedButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-commenting-o')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
var node = button.getNode();
var onunsaved = JX.bind(this, this._onunsavedclick);
JX.DOM.listen(node, 'click', null, onunsaved);
this._unsavedButton = button;
}
return this._unsavedButton;
},
_getUnsubmittedButton: function() {
if (!this._unsubmittedButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-comment-o')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
var node = button.getNode();
var onunsubmitted = JX.bind(this, this._onunsubmittedclick);
JX.DOM.listen(node, 'click', null, onunsubmitted);
this._unsubmittedButton = button;
}
return this._unsubmittedButton;
},
_getDoneButton: function() {
if (!this._doneButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-comment')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
var node = button.getNode();
var ondone = JX.bind(this, this._ondoneclick);
JX.DOM.listen(node, 'click', null, ondone);
this._doneButton = button;
}
return this._doneButton;
},
_getMenuButton: function() {
if (!this._menuButton) {
+ var pht = this.getTranslations();
+
var button = new JX.PHUIXButtonView()
.setIcon('fa-bars')
- .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
+ .setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE)
+ .setAuralLabel(pht('Display Options'));
var dropdown = new JX.PHUIXDropdownMenu(button.getNode());
this._menuItems = {};
var list = new JX.PHUIXActionListView();
dropdown.setContent(list.getNode());
var map = {
hideDone: {
type: 'done'
},
hideCollapsed: {
type: 'collapsed'
},
hideGhosts: {
type: 'ghosts'
},
hideAll: {
type: 'all'
},
showAll: {
type: 'show'
}
};
for (var k in map) {
var spec = map[k];
var handler = JX.bind(this, this._onhideinlines, spec.type);
var item = new JX.PHUIXActionView()
.setHandler(handler);
list.addItem(item);
this._menuItems[k] = item;
}
dropdown.listen('open', JX.bind(this, this._ondropdown));
- var pht = this.getTranslations();
-
if (this.getInlineListURI()) {
list.addItem(
new JX.PHUIXActionView()
.setDivider(true));
list.addItem(
new JX.PHUIXActionView()
.setIcon('fa-external-link')
.setName(pht('List Inline Comments'))
.setHref(this.getInlineListURI()));
}
this._menuButton = button;
this._dropdownMenu = dropdown;
}
return this._menuButton;
},
_ondropdown: function() {
var inlines = this._getInlinesByType();
var items = this._menuItems;
var pht = this.getTranslations();
items.hideDone
.setName(pht('Hide "Done" Inlines'))
.setDisabled(!inlines.visibleDone.length);
items.hideCollapsed
.setName(pht('Hide Collapsed Inlines'))
.setDisabled(!inlines.visibleCollapsed.length);
items.hideGhosts
.setName(pht('Hide Older Inlines'))
.setDisabled(!inlines.visibleGhosts.length);
items.hideAll
.setName(pht('Hide All Inlines'))
.setDisabled(!inlines.visible.length);
items.showAll
.setName(pht('Show All Inlines'))
.setDisabled(!inlines.hidden.length);
},
_onhideinlines: function(type, e) {
this._dropdownMenu.close();
e.prevent();
this._toggleInlines(type);
},
_toggleInlines: function(type) {
var inlines = this._getInlinesByType();
// Clear the selection state since we end up in a weird place if the
// user hides the selected inline.
this._setSelectionState(null);
var targets;
var mode = true;
switch (type) {
case 'done':
targets = inlines.visibleDone;
break;
case 'collapsed':
targets = inlines.visibleCollapsed;
break;
case 'ghosts':
targets = inlines.visibleGhosts;
break;
case 'all':
targets = inlines.visible;
break;
case 'show':
targets = inlines.hidden;
mode = false;
break;
}
for (var ii = 0; ii < targets.length; ii++) {
targets[ii].setHidden(mode);
}
},
_onunsavedclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
show: true,
attribute: 'unsaved'
};
this._onjumpkey(1, options);
},
_onunsubmittedclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
show: true,
attribute: 'anyDraft'
};
this._onjumpkey(1, options);
},
_ondoneclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
show: true,
attribute: this._doneMode
};
this._onjumpkey(1, options);
},
_getBannerNode: function() {
if (!this._bannerNode) {
var attributes = {
className: 'diff-banner',
id: 'diff-banner'
};
this._bannerNode = JX.$N('div', attributes);
}
return this._bannerNode;
},
_getVisibleChangeset: function() {
if (this.isAsleep()) {
return null;
}
if (JX.Device.getDevice() != 'desktop') {
return null;
}
// Never show the banner if we're very near the top of the page.
var margin = 480;
var s = JX.Vector.getScroll();
if (s.y < margin) {
return null;
}
// We're going to find the changeset which spans an invisible line a
// little underneath the bottom of the banner. This makes the header
// tick over from "A.txt" to "B.txt" just as "A.txt" scrolls completely
// offscreen.
var detect_height = 64;
for (var ii = 0; ii < this._changesets.length; ii++) {
var changeset = this._changesets[ii];
var c = changeset.getVectors();
// If the changeset starts above the line...
if (c.pos.y <= (s.y + detect_height)) {
// ...and ends below the line, this is the current visible changeset.
if ((c.pos.y + c.dim.y) >= (s.y + detect_height)) {
return changeset;
}
}
}
return null;
}
}
});
diff --git a/webroot/rsrc/js/application/differential/behavior-populate.js b/webroot/rsrc/js/application/differential/behavior-populate.js
index 0a4a7912e..5b415fd13 100644
--- a/webroot/rsrc/js/application/differential/behavior-populate.js
+++ b/webroot/rsrc/js/application/differential/behavior-populate.js
@@ -1,141 +1,142 @@
/**
* @provides javelin-behavior-differential-populate
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* phabricator-tooltip
* phabricator-diff-changeset-list
* phabricator-diff-changeset
* @javelin
*/
JX.behavior('differential-populate', function(config, statics) {
// When we perform a Quicksand navigation, deactivate the changeset lists on
// the current page and activate the changeset lists on the new page.
var onredraw = function(page_id) {
// If the current page is already active, we don't need to do anything.
if (statics.pageID === page_id) {
return;
}
var ii;
// Put the old lists to sleep.
var old_lists = get_lists(statics.pageID);
for (ii = 0; ii < old_lists.length; ii++) {
old_lists[ii].sleep();
}
statics.pageID = null;
// Awaken the new lists, if they exist.
if (statics.pages.hasOwnProperty(page_id)) {
var new_lists = get_lists(page_id);
for (ii = 0; ii < new_lists.length; ii++) {
new_lists[ii].wake();
}
statics.pageID = page_id;
}
};
// Get changeset lists on the current page.
var get_lists = function(page_id) {
if (page_id === null) {
return [];
}
return statics.pages[page_id] || [];
};
if (!statics.installed) {
statics.installed = true;
statics.pages = {};
statics.pageID = null;
JX.Stratcom.listen('quicksand-redraw', null, function(e) {
onredraw(e.getData().newResponseID);
});
}
var changeset_list = new JX.DiffChangesetList()
.setTranslations(JX.phtize(config.pht))
.setInlineURI(config.inlineURI)
- .setInlineListURI(config.inlineListURI);
+ .setInlineListURI(config.inlineListURI)
+ .setIsStandalone(config.isStandalone);
// Install and activate the current page.
var page_id = JX.Quicksand.getCurrentPageID();
statics.pages[page_id] = [changeset_list];
onredraw(page_id);
for (var ii = 0; ii < config.changesetViewIDs.length; ii++) {
var id = config.changesetViewIDs[ii];
var node = JX.$(id);
var changeset = changeset_list.newChangesetForNode(node);
if (changeset.shouldAutoload()) {
changeset.setStabilize(true).load();
}
}
var highlighted = null;
var highlight_class = null;
JX.Stratcom.listen(
['mouseover', 'mouseout'],
['differential-changeset', 'tag:td'],
function(e) {
var t = e.getTarget();
// NOTE: Using className is not best practice, but the diff UI is perf
// sensitive.
if (!t.className.match(/cov|copy/)) {
return;
}
if (e.getType() == 'mouseout') {
JX.Tooltip.hide();
if (highlighted) {
JX.DOM.alterClass(highlighted, highlight_class, false);
highlighted = null;
}
} else {
highlight_class = null;
var msg;
var align = 'W';
var sibling = 'previousSibling';
var width = 120;
if (t.className.match(/cov-C/)) {
msg = 'Covered';
highlight_class = 'source-cov-C';
} else if (t.className.match(/cov-U/)) {
msg = 'Not Covered';
highlight_class = 'source-cov-U';
} else if (t.className.match(/cov-N/)) {
msg = 'Not Executable';
highlight_class = 'source-cov-N';
} else {
var match = /new-copy|new-move/.exec(t.className);
if (match) {
sibling = 'nextSibling';
width = 500;
msg = JX.Stratcom.getData(t).msg;
highlight_class = match[0];
}
}
if (msg) {
JX.Tooltip.show(t, width, align, msg);
}
if (highlight_class) {
highlighted = t[sibling];
JX.DOM.alterClass(highlighted, highlight_class, true);
}
}
});
});
diff --git a/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js b/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js
deleted file mode 100644
index 41ff0cfcc..000000000
--- a/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * @provides javelin-behavior-diffusion-browse-file
- * @requires javelin-behavior
- * javelin-dom
- * javelin-util
- * phabricator-tooltip
- */
-
-JX.behavior('diffusion-browse-file', function(config, statics) {
- if (statics.installed) {
- return;
- }
- statics.installed = true;
-
- var map = config.labels;
-
- JX.Stratcom.listen(
- ['mouseover', 'mouseout'],
- ['phabricator-source', 'tag:td'],
- function(e) {
- var target = e.getTarget();
-
- // NOTE: We're using raw classnames instead of sigils and metadata here
- // because these elements are unusual: there are a lot of them on the
- // page, and rendering all the extra metadata to do this in a normal way
- // would be needlessly expensive. This is an unusual case.
-
- if (!target.className.match(/cov-/)) {
- return;
- }
-
- if (e.getType() == 'mouseout') {
- JX.Tooltip.hide();
- return;
- }
-
- for (var k in map) {
- if (!target.className.match(k)) {
- continue;
- }
-
- var label = map[k];
- JX.Tooltip.show(target, 300, 'E', label);
- break;
- }
- });
-});
diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js
index 2fc488740..caad5977b 100644
--- a/webroot/rsrc/js/application/files/behavior-document-engine.js
+++ b/webroot/rsrc/js/application/files/behavior-document-engine.js
@@ -1,379 +1,479 @@
/**
* @provides javelin-behavior-document-engine
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
*/
JX.behavior('document-engine', function(config, statics) {
function onmenu(e) {
var node = e.getNode('document-engine-view-dropdown');
var data = JX.Stratcom.getData(node);
if (data.menu) {
return;
}
e.prevent();
var menu = new JX.PHUIXDropdownMenu(node);
var list = new JX.PHUIXActionListView();
var view;
var engines = [];
for (var ii = 0; ii < data.views.length; ii++) {
var spec = data.views[ii];
view = new JX.PHUIXActionView()
.setName(spec.name)
.setIcon(spec.icon)
.setIconColor(spec.color)
.setHref(spec.engineURI);
view.setHandler(JX.bind(null, function(spec, e) {
if (!e.isNormalClick()) {
return;
}
e.prevent();
menu.close();
onview(data, spec, false);
}, spec));
list.addItem(view);
engines.push({
spec: spec,
view: view
});
}
list.addItem(
new JX.PHUIXActionView()
.setDivider(true));
var encode_item = new JX.PHUIXActionView()
.setName(data.encode.name)
.setIcon(data.encode.icon);
var onencode = JX.bind(null, function(data, e) {
e.prevent();
if (encode_item.getDisabled()) {
return;
}
new JX.Workflow(data.encode.uri, {encoding: data.encode.value})
.setHandler(function(r) {
data.encode.value = r.encoding;
onview(data);
})
.start();
menu.close();
}, data);
encode_item.setHandler(onencode);
list.addItem(encode_item);
var highlight_item = new JX.PHUIXActionView()
.setName(data.highlight.name)
.setIcon(data.highlight.icon);
var onhighlight = JX.bind(null, function(data, e) {
e.prevent();
if (highlight_item.getDisabled()) {
return;
}
new JX.Workflow(data.highlight.uri, {highlight: data.highlight.value})
.setHandler(function(r) {
data.highlight.value = r.highlight;
onview(data);
})
.start();
menu.close();
}, data);
highlight_item.setHandler(onhighlight);
list.addItem(highlight_item);
+ var blame_item;
+ if (data.blame.uri) {
+ blame_item = new JX.PHUIXActionView()
+ .setIcon(data.blame.icon);
+
+ var onblame = JX.bind(null, function(data, e) {
+ e.prevent();
+
+ if (blame_item.getDisabled()) {
+ return;
+ }
+
+ data.blame.enabled = !data.blame.enabled;
+ onview(data);
+
+ menu.close();
+ }, data);
+
+ blame_item.setHandler(onblame);
+
+ list.addItem(blame_item);
+ }
+
menu.setContent(list.getNode());
menu.listen('open', function() {
for (var ii = 0; ii < engines.length; ii++) {
var engine = engines[ii];
// Highlight the current rendering engine.
var is_selected = (engine.spec.viewKey == data.viewKey);
engine.view.setSelected(is_selected);
if (is_selected) {
encode_item.setDisabled(!engine.spec.canEncode);
highlight_item.setDisabled(!engine.spec.canHighlight);
+ if (blame_item) {
+ blame_item.setDisabled(!engine.spec.canBlame);
+ }
+ }
+ }
+
+ if (blame_item) {
+ var blame_label;
+ if (data.blame.enabled) {
+ blame_label = data.blame.hide;
+ } else {
+ blame_label = data.blame.show;
}
+
+ blame_item.setName(blame_label);
}
});
data.menu = menu;
menu.open();
}
function add_params(uri, data) {
uri = JX.$U(uri);
if (data.highlight.value) {
uri.setQueryParam('highlight', data.highlight.value);
}
if (data.encode.value) {
uri.setQueryParam('encode', data.encode.value);
}
+ if (data.blame.enabled) {
+ uri.setQueryParam('blame', null);
+ } else {
+ uri.setQueryParam('blame', 'off');
+ }
+
return uri.toString();
}
function onview(data, spec, immediate) {
if (!spec) {
for (var ii = 0; ii < data.views.length; ii++) {
if (data.views[ii].viewKey == data.viewKey) {
spec = data.views[ii];
break;
}
}
}
data.sequence = (data.sequence || 0) + 1;
var handler = JX.bind(null, onrender, data, data.sequence, spec);
data.viewKey = spec.viewKey;
var uri = add_params(spec.engineURI, data);
new JX.Request(uri, handler)
.send();
if (data.loadingView) {
// If we're already showing "Loading...", immediately change it to
// show the new document type.
onloading(data, spec);
} else if (!immediate) {
// Otherwise, grey out the document and show "Loading..." after a
// short delay. This prevents the content from flickering when rendering
// is fast.
var viewport = JX.$(data.viewportID);
JX.DOM.alterClass(viewport, 'document-engine-in-flight', true);
var load = JX.bind(null, onloading, data, spec);
data.loadTimer = setTimeout(load, 333);
// Replace the URI with the URI for the specific rendering the user
// has selected.
var view_uri = add_params(spec.viewURI, data);
JX.History.replace(view_uri);
}
}
function onloading(data, spec) {
data.loadingView = true;
var viewport = JX.$(data.viewportID);
JX.DOM.alterClass(viewport, 'document-engine-in-flight', false);
JX.DOM.setContent(viewport, JX.$H(spec.loadingMarkup));
}
function onrender(data, sequence, spec, r) {
// If this isn't the most recent request we sent, throw it away. This can
// happen if the user makes multiple selections from the menu while we are
// still rendering the first view.
if (sequence != data.sequence) {
return;
}
if (data.loadTimer) {
clearTimeout(data.loadTimer);
data.loadTimer = null;
}
var viewport = JX.$(data.viewportID);
JX.DOM.alterClass(viewport, 'document-engine-in-flight', false);
data.loadingView = false;
JX.DOM.setContent(viewport, JX.$H(r.markup));
// If this engine supports rendering blame, populate or draw it.
- if (spec.canBlame) {
+ if (spec.canBlame && data.blame.enabled) {
blame(data);
}
}
function blame(data) {
// If the rendering engine can't handle blame, bail.
if (!data.blame.uri) {
return;
}
// If we already have an outstanding request for blame data, bail.
if (data.blame.request) {
return;
}
// If we don't have blame data yet, request it and then try rendering
// again later.
if (!data.blame.value) {
var req = new JX.Request(data.blame.uri, JX.bind(null, onblame, data));
data.blame.request = req;
req.send();
return;
}
// We're ready to render.
var viewport = JX.$(data.viewportID);
var row_nodes = JX.DOM.scry(viewport, 'tr');
var row_list = [];
var ii;
for (ii = 0; ii < row_nodes.length; ii++) {
var row = {};
var keep = false;
var node = row_nodes[ii];
for (var jj = 0; jj < node.childNodes.length; jj++) {
var child = node.childNodes[jj];
if (!JX.DOM.isType(child, 'th')) {
continue;
}
var spec = child.getAttribute('data-blame');
if (spec) {
row[spec] = child;
keep = true;
}
if (spec === 'info') {
row.lines = child.getAttribute('data-blame-lines');
}
}
if (keep) {
row_list.push(row);
}
}
var last = null;
for (ii = 0; ii < row_list.length; ii++) {
var commit = data.blame.value.blame[row_list[ii].lines - 1];
row_list[ii].commit = commit;
row_list[ii].last = last;
last = commit;
}
for (ii = 0; ii < row_list.length; ii++) {
renderBlame(row_list[ii], data.blame.value);
}
}
function onblame(data, r) {
data.blame.request = null;
data.blame.value = r;
blame(data);
}
function renderBlame(row, blame) {
var spec = blame.map[row.commit];
var info = null;
var skip = null;
if (spec && (row.commit != row.last)) {
skip = JX.$H(spec.skip);
info = JX.$H(spec.info);
}
if (row.skip) {
JX.DOM.setContent(row.skip, skip);
}
if (row.info) {
JX.DOM.setContent(row.info, info);
}
var epoch_range = (blame.epoch.max - blame.epoch.min);
var epoch_value;
if (!epoch_range) {
epoch_value = 1;
} else {
epoch_value = (spec.epoch - blame.epoch.min) / epoch_range;
}
var h_min = 0.04;
var h_max = 0.44;
var h = h_min + ((h_max - h_min) * epoch_value);
- var s = 0.44;
+ var s = 0.25;
var v_min = 0.92;
var v_max = 1.00;
var v = v_min + ((v_max - v_min) * epoch_value);
row.info.style.background = getHSV(h, s, v);
}
function getHSV(h, s, v) {
var r, g, b, i, f, p, q, t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
r = Math.round(r * 255);
g = Math.round(g * 255);
b = Math.round(b * 255);
return 'rgb(' + r + ', ' + g + ', ' + b + ')';
}
+ function onhovercoverage(data, e) {
+ if (e.getType() === 'mouseout') {
+ redraw_coverage(data, null);
+ return;
+ }
+
+ var target = e.getNode('tag:th');
+ var coverage = target.getAttribute('data-coverage');
+ if (!coverage) {
+ return;
+ }
+
+ redraw_coverage(data, target);
+ }
+
+ var coverage_row = null;
+ function redraw_coverage(data, node) {
+ if (coverage_row) {
+ JX.DOM.alterClass(
+ coverage_row,
+ 'phabricator-source-coverage-highlight',
+ false);
+ coverage_row = null;
+ }
+
+ if (!node) {
+ JX.Tooltip.hide();
+ return;
+ }
+
+ var coverage = node.getAttribute('data-coverage');
+ coverage = coverage.split('/');
+
+ var idx = parseInt(coverage[0], 10);
+ var chr = coverage[1];
+
+ var map = data.coverage.labels[idx];
+ if (map) {
+ var label = map[chr];
+ if (label) {
+ JX.Tooltip.show(node, 300, 'W', label);
+
+ coverage_row = JX.DOM.findAbove(node, 'tr');
+ JX.DOM.alterClass(
+ coverage_row,
+ 'phabricator-source-coverage-highlight',
+ true);
+ }
+ }
+ }
+
if (!statics.initialized) {
JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu);
statics.initialized = true;
}
if (config && config.controlID) {
var control = JX.$(config.controlID);
var data = JX.Stratcom.getData(control);
switch (config.next) {
case 'render':
onview(data, null, true);
break;
case 'blame':
blame(data);
break;
}
+
+ JX.DOM.listen(
+ JX.$(data.viewportID),
+ ['mouseover', 'mouseout'],
+ 'tag:th',
+ JX.bind(null, onhovercoverage, data));
}
});
diff --git a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
index 5bff2c1bb..db60c4705 100644
--- a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
+++ b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
@@ -1,114 +1,114 @@
/**
* @provides javelin-behavior-harbormaster-log
* @requires javelin-behavior
*/
JX.behavior('harbormaster-log', function(config) {
var contentNode = JX.$(config.contentNodeID);
var following = false;
var autoscroll = false;
JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) {
if (!e.isNormalClick()) {
return;
}
e.kill();
- expand(e.getTarget(), true);
+ expand(e.getNode('harbormaster-log-expand'), true);
});
function expand(node, is_action) {
var row = JX.DOM.findAbove(node, 'tr');
row = JX.DOM.findAbove(row, 'tr');
var data = JX.Stratcom.getData(node);
if (data.stop) {
following = false;
autoscroll = false;
JX.DOM.alterClass(contentNode, 'harbormaster-log-following', false);
return;
}
var uri = new JX.URI(config.renderURI)
.addQueryParams(data);
if (data.live && is_action) {
following = true;
autoscroll = true;
JX.DOM.alterClass(contentNode, 'harbormaster-log-following', true);
}
var request = new JX.Request(uri, function(r) {
var result = JX.$H(r.markup).getNode();
var rows = [].slice.apply(result.firstChild.childNodes);
// If we're following the bottom of the log, the result always includes
// the last line from the previous render. Throw it away, then add the
// new data.
if (data.live && row.previousSibling) {
JX.DOM.remove(row.previousSibling);
}
JX.DOM.replace(row, rows);
if (data.live) {
// If this was a live follow, scroll the new data into view. This is
// probably intensely annoying in practice but seems cool for now.
if (autoscroll) {
var last_row = rows[rows.length - 1];
var tail_pos = JX.$V(last_row).y + JX.Vector.getDim(last_row).y;
var view_y = JX.Vector.getViewport().y;
JX.DOM.scrollToPosition(null, (tail_pos - view_y) + 32);
// This will fire a scroll event, but we want to keep autoscroll
// enabled until we see an explicit scroll event by the user.
setTimeout(function() { autoscroll = true; }, 0);
}
setTimeout(follow, 2000);
for (var ii = 1; ii < (rows.length - 1); ii++) {
JX.DOM.alterClass(rows[ii], 'harbormaster-log-appear', true);
}
}
});
request.send();
}
// If the user explicitly scrolls while following a log, keep live updating
// it but stop following it with the scrollbar.
JX.Stratcom.listen('scroll', null, function() {
autoscroll = false;
});
function follow() {
if (!following) {
return;
}
var live;
try {
live = JX.DOM.find(contentNode, 'a', 'harbormaster-log-live');
} catch (e) {
return;
}
expand(live);
}
function onresponse(r) {
JX.DOM.alterClass(contentNode, 'harbormaster-log-view-loading', false);
JX.DOM.setContent(contentNode, JX.$H(r.markup));
}
var uri = new JX.URI(config.initialURI);
new JX.Request(uri, onresponse)
.send();
});
diff --git a/webroot/rsrc/js/application/herald/PathTypeahead.js b/webroot/rsrc/js/application/herald/PathTypeahead.js
index 0ce9823d1..eed6e62b6 100644
--- a/webroot/rsrc/js/application/herald/PathTypeahead.js
+++ b/webroot/rsrc/js/application/herald/PathTypeahead.js
@@ -1,216 +1,214 @@
/**
* @requires javelin-install
* javelin-typeahead
* javelin-dom
* javelin-request
* javelin-typeahead-ondemand-source
* javelin-util
* @provides path-typeahead
* @javelin
*/
JX.install('PathTypeahead', {
construct : function(config) {
this._repositoryTokenizer = config.repositoryTokenizer;
this._hardpoint = config.hardpoint;
this._input = config.path_input;
this._completeURI = config.completeURI;
this._validateURI = config.validateURI;
this._errorDisplay = config.error_display;
this._textInputValues = {};
this._icons = config.icons;
this._initializeDatasource();
this._initializeTypeahead(this._input);
},
members : {
_repositoryTokenizer : null,
/*
* DOM parent div "hardpoint" to be passed to the JX.Typeahead.
*/
_hardpoint : null,
/*
* DOM element to display errors.
*/
_errorDisplay : null,
/*
* URI to query for typeahead results, to be passed to the
* TypeaheadOnDemandSource.
*/
_completeURI : null,
/*
* Underlying JX.TypeaheadOnDemandSource instance
*/
_datasource : null,
/*
* Underlying JX.Typeahead instance
*/
_typeahead : null,
/*
* Underlying input
*/
_input : null,
/*
* Whenever the user changes the typeahead value, we track the change
* here, keyed by the selected repository ID. That way, we can restore
* typed values if they change the repository choice and then change back.
*/
_textInputValues : null,
/*
* Configurable endpoint for server-side path validation
*/
_validateURI : null,
/*
* Keep the validation AJAX request so we don't send several.
*/
_validationInflight : null,
/*
* Installs path-specific behaviors and then starts the underlying
* typeahead.
*/
start : function() {
if (this._typeahead.getValue()) {
var phid = this._getRepositoryPHID();
if (phid) {
this._textInputValues[phid] = this._typeahead.getValue();
}
}
this._typeahead.listen(
'change',
JX.bind(this, function(value) {
var phid = this._getRepositoryPHID();
if (phid) {
this._textInputValues[phid] = value;
}
this._validate();
}));
var repo_set_input = JX.bind(this, this._onrepochange);
this._typeahead.listen('start', repo_set_input);
this._repositoryTokenizer.listen('change', repo_set_input);
this._typeahead.start();
this._validate();
},
_onrepochange : function() {
this._setPathInputBasedOnRepository(
this._typeahead,
this._textInputValues);
this._datasource.setAuxiliaryData(
{
repositoryPHID: this._getRepositoryPHID()
});
// Since we've changed the repository, reset the results.
this._datasource.resetResults();
},
_setPathInputBasedOnRepository : function(typeahead, lookup) {
var phid = this._getRepositoryPHID();
if (phid && lookup[phid]) {
typeahead.setValue(lookup[phid]);
} else {
typeahead.setValue('/');
}
},
_initializeDatasource : function() {
this._datasource = new JX.TypeaheadOnDemandSource(this._completeURI);
this._datasource.setNormalizer(this._datasourceNormalizer);
this._datasource.setQueryDelay(40);
},
/*
* Construct and initialize the Typeahead.
* Must be called after initializing the datasource.
*/
_initializeTypeahead : function(path_input) {
this._typeahead = new JX.Typeahead(this._hardpoint, path_input);
this._datasource.setMaximumResultCount(15);
this._typeahead.setDatasource(this._datasource);
},
_datasourceNormalizer : function(str) {
return ('' + str).replace(/[\/]+/g, '\/');
},
_getRepositoryPHID: function() {
var tokens = this._repositoryTokenizer.getTokens();
var keys = JX.keys(tokens);
if (keys.length) {
return keys[0];
}
return null;
},
_validate : function() {
var repo_phid = this._getRepositoryPHID();
if (!repo_phid) {
return;
}
var input = this._input;
var input_value = input.value;
var error_display = this._errorDisplay;
if (!input_value.length) {
input.value = '/';
input_value = '/';
}
if (this._validationInflight) {
this._validationInflight.abort();
this._validationInflight = null;
}
var validation_request = new JX.Request(
this._validateURI,
JX.bind(this, function(payload) {
// Don't change validation display state if the input has been
// changed since we started validation
if (input.value !== input_value) {
return;
}
if (payload.valid) {
JX.DOM.setContent(error_display, JX.$H(this._icons.okay));
} else {
JX.DOM.setContent(error_display, JX.$H(this._icons.fail));
}
}));
validation_request.listen('finally', function() {
this._validationInflight = null;
});
validation_request.setData(
{
repositoryPHID : repo_phid,
path : input_value
});
this._validationInflight = validation_request;
JX.DOM.setContent(error_display, JX.$H(this._icons.test));
-
- validation_request.setTimeout(750);
validation_request.send();
}
}
});
diff --git a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js
index 11141f138..3bfcd6c8d 100644
--- a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js
+++ b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js
@@ -1,261 +1,264 @@
/**
* @provides javelin-behavior-comment-actions
* @requires javelin-behavior
* javelin-stratcom
* javelin-workflow
* javelin-dom
* phuix-form-control-view
* phuix-icon-view
* javelin-behavior-phabricator-gesture
*/
JX.behavior('comment-actions', function(config) {
var action_map = config.actions;
var action_node = JX.$(config.actionID);
var form_node = JX.$(config.formID);
var input_node = JX.$(config.inputID);
var place_node = JX.$(config.placeID);
var rows = {};
JX.DOM.listen(action_node, 'change', null, function() {
var option = find_option(action_node.value);
action_node.value = '+';
if (option) {
add_row(option);
}
});
function find_option(key) {
var options = action_node.options;
var option;
for (var ii = 0; ii < options.length; ii++) {
option = options[ii];
if (option.value == key) {
return option;
}
}
return null;
}
function remove_action(key) {
var row = rows[key];
if (row) {
JX.DOM.remove(row.node);
row.option.disabled = false;
delete rows[key];
}
}
function serialize_actions() {
var data = [];
for (var k in rows) {
data.push({
type: k,
value: rows[k].control.getValue(),
initialValue: action_map[k].initialValue || null
});
}
return JX.JSON.stringify(data);
}
function get_data() {
var data = JX.DOM.convertFormToDictionary(form_node);
data.__preview__ = 1;
data[input_node.name] = serialize_actions();
return data;
}
function restore_draft_actions(drafts) {
var draft;
var option;
var control;
for (var ii = 0; ii < drafts.length; ii++) {
draft = drafts[ii];
option = find_option(draft);
if (!option) {
continue;
}
control = add_row(option);
}
}
function onresponse(response) {
if (JX.Device.getDevice() != 'desktop') {
return;
}
var panel = JX.$(config.panelID);
if (!response.xactions.length) {
JX.DOM.hide(panel);
} else {
var preview_root = JX.$(config.timelineID);
JX.DOM.setContent(
preview_root,
[
JX.$H(response.xactions.join('')),
JX.$H(response.previewContent)
]);
JX.DOM.show(panel);
// NOTE: Resonses are currently processed before associated behaviors are
// registered. We need to defer invoking this event so that any behaviors
// accompanying the response are registered.
var invoke_preview = function() {
JX.Stratcom.invoke(
'EditEngine.didCommentPreview',
null,
{
rootNode: preview_root
});
};
setTimeout(invoke_preview, 0);
}
}
function force_preview() {
if (!config.showPreview) {
return;
}
new JX.Request(config.actionURI, onresponse)
.setData(get_data())
.send();
}
function add_row(option) {
var action = action_map[option.value];
if (!action) {
return;
}
// Remove any conflicting actions. For example, "Accept Revision" conflicts
// with "Reject Revision".
var conflict_key = action.conflictKey || null;
if (conflict_key !== null) {
for (var k in action_map) {
if (k === action.key) {
continue;
}
if (action_map[k].conflictKey !== conflict_key) {
continue;
}
if (!(k in rows)) {
continue;
}
remove_action(k);
}
}
option.disabled = true;
+ var aural = JX.$N('span', {className: 'aural-only'}, action.auralLabel);
+
var icon = new JX.PHUIXIconView()
.setIcon('fa-times-circle');
- var remove = JX.$N('a', {href: '#'}, icon.getNode());
+
+ var remove = JX.$N('a', {href: '#'}, [aural, icon.getNode()]);
var control = new JX.PHUIXFormControl()
.setLabel(action.label)
.setError(remove)
.setControl(action.type, action.spec)
.setClass('phui-comment-action');
var node = control.getNode();
JX.Stratcom.addSigil(node, 'touchable');
JX.DOM.listen(node, 'gesture.swipe.end', null, function(e) {
var data = e.getData();
if (data.direction != 'left') {
// Didn't swipe left.
return;
}
if (data.length <= (JX.Vector.getDim(node).x / 2)) {
// Didn't swipe far enough.
return;
}
remove_action(action.key);
});
rows[action.key] = {
control: control,
node: node,
option: option
};
JX.DOM.listen(remove, 'click', null, function(e) {
e.kill();
remove_action(action.key);
});
place_node.parentNode.insertBefore(node, place_node);
force_preview();
return control;
}
JX.DOM.listen(form_node, ['submit', 'didSyntheticSubmit'], null, function() {
input_node.value = serialize_actions();
});
if (config.showPreview) {
var request = new JX.PhabricatorShapedRequest(
config.actionURI,
onresponse,
get_data);
var trigger = JX.bind(request, request.trigger);
JX.DOM.listen(form_node, 'keydown', null, trigger);
JX.DOM.listen(form_node, 'shouldRefresh', null, force_preview);
request.start();
var old_device = JX.Device.getDevice();
var ondevicechange = function() {
var new_device = JX.Device.getDevice();
var panel = JX.$(config.panelID);
if (new_device == 'desktop') {
request.setRateLimit(500);
// Force an immediate refresh if we switched from another device type
// to desktop.
if (old_device != new_device) {
force_preview();
}
} else {
// On mobile, don't show live previews and only save drafts every
// 10 seconds.
request.setRateLimit(10000);
JX.DOM.hide(panel);
}
old_device = new_device;
};
ondevicechange();
JX.Stratcom.listen('phabricator-device-change', null, ondevicechange);
}
restore_draft_actions(config.drafts || []);
});
diff --git a/webroot/rsrc/js/core/behavior-phabricator-nav.js b/webroot/rsrc/js/core/behavior-phabricator-nav.js
index cf6dc880b..8bc4ad04e 100644
--- a/webroot/rsrc/js/core/behavior-phabricator-nav.js
+++ b/webroot/rsrc/js/core/behavior-phabricator-nav.js
@@ -1,206 +1,223 @@
/**
* @provides javelin-behavior-phabricator-nav
* @requires javelin-behavior
* javelin-behavior-device
* javelin-stratcom
* javelin-dom
* javelin-magical-init
* javelin-vector
* javelin-request
* javelin-util
* @javelin
*/
JX.behavior('phabricator-nav', function(config) {
var content = JX.$(config.contentID);
var local = JX.$(config.localID);
var main = JX.$(config.mainID);
var drag = JX.$(config.dragID);
// - Flexible Navigation Column ------------------------------------------------
-
var dragging;
var track;
+ var collapsed = config.collapsed;
+ var narrowed;
+ var visible = null;
+
JX.enableDispatch(document.body, 'mousemove');
JX.DOM.listen(drag, 'mousedown', null, function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
dragging = JX.$V(e);
// Show the "col-resize" cursor on the whole document while we're
// dragging, since the mouse will slip off the actual bar fairly often and
// we don't want it to flicker.
JX.DOM.alterClass(document.body, 'jx-drag-col', true);
track = [
{
element: local,
parameter: 'width',
start: JX.Vector.getDim(local).x,
width: JX.Vector.getDim(local).x,
minWidth: 1
},
{
element: drag,
parameter: 'left',
start: get_width()
},
{
element: content,
parameter: 'marginLeft',
start: parseInt(getComputedStyle(content).marginLeft, 10),
width: JX.Vector.getDim(content).x,
minWidth: 300,
minScale: -1
}
];
e.kill();
});
JX.Stratcom.listen('mousemove', null, function(e) {
if (!dragging) {
return;
}
var dx = JX.$V(e).x - dragging.x;
var panel;
var k;
for (k = 0; k < track.length; k++) {
panel = track[k];
if (!panel.minWidth) {
continue;
}
var new_width = panel.width + (dx * (panel.minScale || 1));
if (new_width < panel.minWidth) {
dx = (panel.minWidth - panel.width) * panel.minScale;
}
}
for (k = 0; k < track.length; k++) {
panel = track[k];
var v = (panel.start + (dx * (panel.scale || 1)));
panel.element.style[panel.parameter] = v + 'px';
}
});
JX.Stratcom.listen('mouseup', null, function() {
if (!dragging) {
return;
}
+
JX.DOM.alterClass(document.body, 'jx-drag-col', false);
dragging = false;
new JX.Request('/settings/adjust/', JX.bag)
.setData(
{
key: 'filetree.width',
value: get_width()
})
.send();
});
function get_width() {
// See PHI568. If the document has scrolled horizontally, the "x" position
// of the bar will be the actual width of the menu plus the horizontal
// scroll position (because the element is "position: fixed"). Subtract the
// document scroll position when saving the element width so that scrolling
// to the right and then toggling the filetree UI does not make it grow
// any wider.
return (JX.$V(drag).x - JX.Vector.getScroll().x);
}
+ function repaint() {
+ narrowed = !JX.Device.isDesktop();
+
+ var was_visible = visible;
+ visible = (!collapsed && !narrowed);
+
+ if (was_visible === visible) {
+ return;
+ }
+
+ if (!visible) {
+ savedrag();
+ }
+
+ JX.DOM.alterClass(main, 'has-local-nav', visible);
+ JX.DOM.alterClass(main, 'has-drag-nav', visible);
+ JX.DOM.alterClass(main, 'has-closed-nav', !visible);
+
+ if (visible) {
+ restoredrag();
+ }
+ }
+
var saved_width = config.width;
function savedrag() {
saved_width = get_width();
local.style.width = '';
drag.style.left = '';
content.style.marginLeft = '';
}
function restoredrag() {
if (!saved_width) {
return;
}
local.style.width = saved_width + 'px';
drag.style.left = saved_width + 'px';
content.style.marginLeft = (saved_width + JX.Vector.getDim(drag).x) + 'px';
}
- var collapsed = config.collapsed;
JX.Stratcom.listen('differential-filetree-toggle', null, function() {
collapsed = !collapsed;
- if (collapsed) {
- savedrag();
- }
-
- JX.DOM.alterClass(main, 'has-local-nav', !collapsed);
- JX.DOM.alterClass(main, 'has-drag-nav', !collapsed);
- JX.DOM.alterClass(main, 'has-closed-nav', collapsed);
-
- if (!collapsed) {
- restoredrag();
- }
+ repaint();
new JX.Request('/settings/adjust/', JX.bag)
.setData({ key : 'nav-collapsed', value : (collapsed ? 1 : 0) })
.send();
// Invoke a resize event so page elements can redraw if they need to. One
// example is the selection reticles in Differential.
JX.Stratcom.invoke('resize');
});
// - Scroll --------------------------------------------------------------------
// When the user scrolls or resizes the window, anchor the menu to to the top
// of the navigation bar.
function onresize() {
- if (JX.Device.getDevice() != 'desktop') {
+ repaint();
+
+ if (!visible) {
return;
}
// When the buoyant header is visible, move the menu down below it. This
// is a bit of a hack.
var banner_height = 0;
try {
var banner = JX.$('diff-banner');
banner_height = JX.Vector.getDim(banner).y;
} catch (error) {
// Ignore if there's no banner on the page.
}
local.style.top = Math.max(
0,
banner_height,
JX.$V(content).y - Math.max(0, JX.Vector.getScroll().y)) + 'px';
}
local.style.position = 'fixed';
local.style.bottom = 0;
local.style.left = 0;
JX.Stratcom.listen(['scroll', 'resize'], null, onresize);
- onresize();
+ repaint();
// - Navigation Reset ----------------------------------------------------------
JX.Stratcom.listen('phabricator-device-change', null, function() {
- resetdrag();
- onresize();
+ repaint();
});
});
diff --git a/webroot/rsrc/js/phuix/PHUIXButtonView.js b/webroot/rsrc/js/phuix/PHUIXButtonView.js
index b7ca44a67..1205ad3d9 100644
--- a/webroot/rsrc/js/phuix/PHUIXButtonView.js
+++ b/webroot/rsrc/js/phuix/PHUIXButtonView.js
@@ -1,119 +1,144 @@
/**
* @provides phuix-button-view
* @requires javelin-install
* javelin-dom
*/
JX.install('PHUIXButtonView', {
statics: {
BUTTONTYPE_DEFAULT: 'buttontype.default',
BUTTONTYPE_SIMPLE: 'buttontype.simple'
},
members: {
_node: null,
_textNode: null,
+ _auralNode: null,
_iconView: null,
_color: null,
_selected: null,
_buttonType: null,
setIcon: function(icon) {
this.getIconView().setIcon(icon);
return this;
},
getIconView: function() {
if (!this._iconView) {
this._iconView = new JX.PHUIXIconView();
this._redraw();
}
return this._iconView;
},
setColor: function(color) {
var node = this.getNode();
if (this._color) {
JX.DOM.alterClass(node, 'button-' + this._color, false);
}
this._color = color;
JX.DOM.alterClass(node, 'button-' + this._color, true);
return this;
},
setSelected: function(selected) {
var node = this.getNode();
this._selected = selected;
JX.DOM.alterClass(node, 'selected', this._selected);
return this;
},
setButtonType: function(button_type) {
var self = JX.PHUIXButtonView;
this._buttonType = button_type;
var node = this.getNode();
var is_simple = (this._buttonType == self.BUTTONTYPE_SIMPLE);
JX.DOM.alterClass(node, 'phui-button-simple', is_simple);
return this;
},
setText: function(text) {
JX.DOM.setContent(this._getTextNode(), text);
this._redraw();
return this;
},
+ setAuralLabel: function(label) {
+ JX.DOM.setContent(this._getAuralNode(), label);
+ this._redraw();
+ return this;
+ },
+
getNode: function() {
if (!this._node) {
var attrs = {
className: 'button'
};
this._node = JX.$N('button', attrs);
this._redraw();
}
return this._node;
},
_getTextNode: function() {
if (!this._textNode) {
var attrs = {
className: 'phui-button-text'
};
this._textNode = JX.$N('div', attrs);
}
return this._textNode;
},
+ _getAuralNode: function() {
+ if (!this._auralNode) {
+ var attrs = {
+ className: 'aural-only'
+ };
+
+ this._auralNode = JX.$N('span', attrs);
+ }
+
+ return this._auralNode;
+ },
+
_redraw: function() {
var node = this.getNode();
+ var aural = this._auralNode;
var icon = this._iconView;
var text = this._textNode;
var content = [];
+
+ if (aural) {
+ content.push(aural);
+ }
+
if (icon) {
content.push(icon.getNode());
}
if (text) {
content.push(text);
}
JX.DOM.alterClass(node, 'has-icon', !!icon);
JX.DOM.alterClass(node, 'has-text', !!text);
JX.DOM.setContent(node, content);
}
}
});
Event Timeline
Log In to Comment